typescript-generatorプラグインでJavadocからTSDocを生成する

以前調査したtypescript-generator、その後もパラメーター調整しながら使用しているが、困ったのがヒアドキュメント。

Java側ではJavadocでクラスやプロパティの説明を書いているが、生成したTypeScriptのインターフェース定義ファイルにはデフォルトだと出力されないため、フロント実装時にJava側の定義を見ないと使いづらい。

Swaggerアノテーション@ApiModel をクラスに、 @ApiModelProperty をプロパティに付与すると、TypeScript定義ファイルに出力されるが、JavadocとSwaggerアノテーション、両方書くのは面倒。

JavadocからSwagger UIを生成することはSwagger Docletでできそうだが、Swaggerアノテーションを生成するわけではないし、Javadoc書かずにSwaggerアノテーションだけ書くのも何だし、と思っていたら、typescript-generator自体が、xml-docletを使ってJavadocからTSDocを生成する機能を持っていたのでメモ。

環境

OpenJDK 8 8.275.01, typescript-generator 2.27.744, xml-doclet 1.0.5

TSDoc生成処理の概要

Javadoc · vojtechhabarta/typescript-generator Wiki · GitHub に設定方法の記載がある。

まず、xml-docletを使ってXML形式のJavadocを出力し、それをtypescript-generatorに読み込ませれば、パースしてTSDocとして出力してくれる。

設定

Gradleへのxml-docletの導入

まずはxml-docletの導入から。

プロジェクト管理ツールにMavenを使っている場合は、xml-docletのREADME、およびtypescript-generatorのWikiを見ればいいが、Gradleの場合の例がないのでやや面倒。

Javadocツールはドックレットを切り替えることで、出力形式を変更できる。GradleでJavadocタスクにカスタムドックレットを指定する方法はこちら

  1. javadoc タスクを有効化するため、 java プラグインを有効化
  2. configurations でドックレット名を指定
  3. dependencies でドックレットの依存性を追加
    • この時、 implementation など指定するところをドックレット名にする
  4. type: Javadoc となるタスクを追加

java プラグインはもともと入っていたため、以下の設定を追加。

configurations {
  xmlDoclet
}

dependencies {
  xmlDoclet 'com.github.markusbernhardt:xml-doclet:1.0.5'
}

final def tsOutputDir = 'build/typescript-generator'
final def tsXmlDocletDir = "${tsOutputDir}/xml-doclet"

task xmlDoclet(type: Javadoc) {
  source = sourceSets.main.allJava
  destinationDir = (tsXmlDocletDir as File).absoluteFile
  title = null // 「-doctitleは無効なフラグです」でエラーとなるため抑制
  options.noTimestamp = false // 既に廃止済みのオプションがデフォルトで付与され、エラーとなるため抑制
  options.encoding = 'UTF-8' // XMLの出力文字コード
  options.showAll() // publicなフィールド・メソッド以外のJavadocも出力
  options.doclet = 'com.github.markusbernhardt.xmldoclet.XmlDoclet'
  options.docletpath = configurations.xmlDoclet.files as List
}

xml-docletは3年前で更新が止まっており、デフォルトだとOpenJDKのjavadocツールと非互換が出ていたため、一部のオプションを指定して抑制している。

また、 showAll() しているのは、getterがあるがJavadocがない場合、フィールドのJavadocを元にTSDocを出力してくれるため。Lombokを使っている場合に有効。

出力ファイル名のデフォルトは javadoc.xml 。前述の設定で gradle xmlDoclet を実行すると、 build/typescript-generator/xml-doclet/javadoc.xml が出力される。

ちなみに、処理の詳細としては、 build/tmp/xmlDoclet/javadoc.options にファイルが出力され、 javadoc @build/tmp/xmlDoclet/javadoc.options のようにJavadocがオプションファイル指定で実行される模様。

typescript-generatorにjavadoc.xmlを読み込ませる

generateTypeScript タスクの javadocXmlFiles に、読み込ませるXMLファイルのパスをFileのListとして設定するだけで、TSDocを出力してくれる。

また、 generateTypeScript.dependsOnXML出力タスクを追加しておくと gradle generateTypeScript で合わせて実行できる。

generateTypeScript {
  ...
  javadocXmlFiles = ["${tsXmlDocletDir}/javadoc.xml" as File]
}

generateTypeScript.dependsOn compileJava, xmlDoclet

build.gradleの例

typescript-generatorの設定を含めた build.gradle は、以下のようになる。いまだに apply plugin なのがちょっと残念。

apply plugin: 'java'
apply plugin: 'cz.habarta.typescript-generator'

buildscript {
  dependencies {
    classpath 'cz.habarta.typescript-generator:typescript-generator-gradle-plugin:2.27.744'
  }
}

configurations {
  xmlDoclet
}

dependencies {
  xmlDoclet 'com.github.markusbernhardt:xml-doclet:1.0.5'
}

final def tsOutputDir = 'build/typescript-generator'
final def tsXmlDocletDir = "${tsOutputDir}/xml-doclet"

generateTypeScript {
  ...
  javadocXmlFiles = ["${tsXmlDocletDir}/javadoc.xml" as File]
}

task xmlDoclet(type: Javadoc) {
  source = sourceSets.main.allJava
  destinationDir = (tsXmlDocletDir as File).absoluteFile
  title = null
  options.noTimestamp = false
  options.encoding = 'UTF-8'
  options.showAll()
  options.doclet = 'com.github.markusbernhardt.xmldoclet.XmlDoclet'
  options.doclet = 'com.github.markusbernhardt.xmldoclet.XmlDoclet'
}

generateTypeScript.dependsOn compileJava, xmlDoclet

出力例

以下のクラスを用意。

/** インターフェース */
public interface ExampleInterface {
    /**
     * インターフェースのメソッド
     *
     * @return 文字列
     */
    String getInterfaceMethod();
}

/** クラス */
@lombok.Data
public class ExampleClass implements ExampleInterface {
    /** クラスのフィールド */
    private String classField;
    private String interfaceMethod;
}

/** 列挙型 */
public enum ExampleEnum {
  /** 1 */
  ONE,
  /** 2 */
  TWO,
  /** 3 */
  THREE;
}

これに対して、前述の build.gradlegenerateTypeScript タスクを以下のように変更して、 gradle generateTypeScript してみる。

generateTypeScript {
  jsonLibrary = 'jackson2'
  classPatterns = [各クラスのパッケージ]
  outputKind = 'module'
  outputFileType = 'implementationFile'
  outputFile = "${tsOutputDir}/example.ts"
  sortDeclarations = true
  stringQuotes = 'singleQuotes'
  indentString = '  '
  javadocXmlFiles = ["${tsXmlDocletDir}/javadoc.xml" as File]
}

結果は以下のようになる(ヘッダのコメント部分は省略)。

/**
 * クラス
 */
export interface ExampleClass extends ExampleInterface {
  /**
   * クラスのフィールド
   */
  classField: string;
}

/**
 * インターフェース
 */
export interface ExampleInterface {
  /**
   * インターフェースのメソッド
   * @return 文字列
   */
  interfaceMethod: string;
}

/**
 * 列挙型
 * 
 * Values:
 * - `ONE` - 1
 * - `TWO` - 2
 * - `THREE` - 3
 */
export type ExampleEnum = 'ONE' | 'TWO' | 'THREE';

XMLをパースしているためか、元のJavadocワンライナーで記述しても、TSDocの出力は複数行になっているなど、多少形式に違いはあるが、概要以外に @return なども出力される。

EnumはデフォルトではUnionにマッピングされるため、typeのTSDocとして各値の説明が出力される。

generateTypeScript のタスク定義に mapEnum = 'asEnum' を追加し、Enumマッピングすると、以下のように、各値にもTSDocが追加される。

/**
 * 列挙型
 * 
 * Values:
 * - `ONE` - 1
 * - `TWO` - 2
 * - `THREE` - 3
 */
export const enum ExampleEnum {
  /**
   * 1
   */
  ONE = 'ONE',
  /**
   * 2
   */
  TWO = 'TWO',
  /**
   * 3
   */
  THREE = 'THREE',
}

振り返り

Java側のコードに手を入れることなく、Gradleタスクを追加するだけでJavadocからTSDocへの変換を行えるようになった。

ただ、xml-docletが更新停止状態で、JDK11以降の新しいJavadoc APIに対応していない模様。

github.com

typescript-generatorの開発者の方がPull Request出したりしているが動きなし。

github.com

先々まで使い続けることはできないかもしれないのが難点か。