※この記事はLiferayコミュニティブログに投稿された、_Creating Headless APIs (Part 2)_を翻訳したものです。
はじめに
このシリーズのパート1では、LiferayのREST Builderツールを使用して独自のカスタムヘッドレスAPIを構築するプロジェクトを開始しました。プロジェクトを開始し、4つのモジュールが作成され、OpenAPI YamlファイルのMetaセクションとReusable Componentsセクションについて紹介しました。
前回の続きとなる本記事では、Paths(エンドポイント)セクションに進み、コードの生成に入ります。
パスの定義
パスはAPIのRESTエンドポイントです。これらの定義はRESTエンドポイントを作成するうえで重要な部分です。この工程に誤りがあると、リファクタリングや将来的に破壊的な変更の原因となり、サービス利用者に不便を強いてしまう可能性が生じます。実際、RESTエンドポイントは適切でない定義になりがちです。REST実装例のバッドプラクティスは非常にたくさんあふれているため、正しいものを見つけたときは驚かされます。
リソース用に選択されたパスには、次の2つの形式があります。
- /v1.0/vitamins
- /v1.0/vitamins/{vitaminId}
1番目の形式はコレクションの取得または新規レコードの作成(使用するHTTPメソッドに準ずる)で、2番目の形式は主キーが指定された特定レコードの取得、更新および削除です。
以下の定義では、レスポンスはすべてハッピーパスレスポンスを指します。そのためgetVitamin
は、Vitaminオブジェクトでの成功したレスポンスのみを提供します。 OpenAPI、とりわけLiferayフレームワークをすべてのパスで活用しているため、エラーや例外を含む可能性のある応答のセットが大きくなることに留意しておく必要があります。フレームワークがそれらすべてを処理するため、成功した応答にのみ関心を払う必要があります。
すべてのビタミンのリスト
したがって最初のパスは、ビタミン/ミネラルのリストの取得に使用されるパスであり、一度にリスト全体を返すのではなく、ページングを使用します。
paths:
"/vitamins":
get:
tags: ["Vitamin"]
description: Retrieves the list of vitamins and minerals. Results can be paginated, filtered, searched, and sorted.
parameters:
- in: query
name: filter
schema:
type: string
- in: query
name: page
schema:
type: integer
- in: query
name: pageSize
schema:
type: integer
- in: query
name: search
schema:
type: string
- in: query
name: sort
schema:
type: string
responses:
200:
description: ""
content:
application/json:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
application/xml:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
/vitamins
に対するGETリクエストは、Vitaminオブジェクトの配列を返します。 Swagger側では、必要なページングの詳細で配列をラップするPageVitamin
という別のコンポーネントタイプが実際に表示されます。
注:ここでのtags属性は重要です。この値は、パスが動作するコンポーネントタイプまたは返されるコンポーネントタイプに一致します。私の方法はすべてVitaminコンポーネントを扱うため、すべてのタグの値は同じ["Vitamin"]
となります。これは、コード生成に絶対に必要です。
多くのLiferay Headless APIと同様に、検索、フィルター、ページングの制御および項目の並べ替えもサポートします。
ビタミンの作成
同じパスにPOSTメソッドを用いて、新しいビタミン/ミネラルオブジェクトを作成できます。
post:
tags: ["Vitamin"]
description: Create a new vitamin/mineral.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
リクエストのボディは作成されるビタミンオブジェクトになり、レスポンスは新たに作成されたインスタンスになります。
ビタミンの取得
2番目のURLフォームは、単一レコードに対して機能します。最初の例では、GETリクエストは指定されたvitaminId
を持つ単一のVitaminオブジェクトを取得します。
"/vitamins/{vitaminId}":
get:
tags: ["Vitamin"]
description: Retrieves the vitamin/mineral via its ID.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
ビタミンの交換
PUTリクエストを使用して現在のビタミンオブジェクトをリクエストボディに含まれるオブジェクトに置き換えることができます。リクエストに含まれていないフィールドは、置換されるレコードの空白またはnull
にする必要があります。
put:
tags: ["Vitamin"]
description: Replaces the vitamin/mineral with the information sent in the request body. Any missing fields are deleted, unless they are required.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: Default Response
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
リクエストには既存のものと置き換えるビタミンが含まれ、レスポンスは新しいビタミンオブジェクトとなります。
ビタミンの更新
PATCHリクエストを使用して現在のビタミンを更新することもできます。提供されないフィールドを空白にするPUTとは異なり、PATCHではリクエストの一部でないフィールドは、該当オブジェクトでは変更されません。
patch:
tags: ["Vitamin"]
description: Replaces the vitamin/mineral with the information sent in the request body. Any missing fields are deleted, unless they are required.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
リクエストには更新するビタミンのフィールドが含まれ、レスポンスは更新されたビタミンオブジェクトとなります。
ビタミンを削除する
最後は、DELETEリクエストを使用してビタミンを削除するパスです。
delete:
tags: ["Vitamin"]
description: Deletes the vitamin/mineral and returns a 204 if the operation succeeds.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
responses:
204:
description: ""
content:
application/json: {}
このパスにはリクエストボディもレスポンスボディもありません。
結果の確認
Swagger Editorを使用してAPIを定義すると、サービスがどのように機能しているかをわかりやすくレビューできます。
上の図のように、視覚的に理解しやすいです!
Yamlファイルの作成に際しては、エディタ自体にコンテキスト依存ヘルプ、構文エラーに対する即時フィードバックなどの優れた機能が用意されており、APIの構成を理解するのに非常に役立ちます。
Swagger Editorを使用する場合は、YamlファイルをIDEへ移すことをお忘れなく。
Swagger Hubで「Resolved Yaml」オプションを使用してエクスポートすると、Yamlエクスポートの最良の結果が得られます。
コード生成試行その1
新しいREST Builderを呼び出す準備が整いました。 headless-vitamins-impl
ディレクトリで次のコマンドを実行します。
$ ../../../gradlew buildREST
私と同様、あなたも失敗に終わったかもしれません。buildREST
を初めて実行したときの出力の一部を次に示します。
Exception in thread "main" Cannot create property=paths for JavaBean=com.liferay.portal.vulcan.yaml.openapi.OpenAPIYAML@1e730495
in 'string', line 1, column 1:
openapi: 3.0.1
^
Cannot create property=get for JavaBean=com.liferay.portal.vulcan.yaml.openapi.PathItem@23f7d05d
in 'string', line 8, column 5:
get:
^
Cannot create property=responses for JavaBean=com.liferay.portal.vulcan.yaml.openapi.Get@23986957
in 'string', line 9, column 7:
tags:
^
For input string: "default"
in 'string', line 36, column 9:
default:
^
[...省略...]
> Task :modules:headless-vitamins-impl:buildREST FAILED
失敗したのはなぜでしょう?メッセージは具体性に欠けており、正確に原因を把握できません。
OpenAPI Yamlファイルを今一度見てみましょう。 Swagger Editorでは問題なく表示されることから、コンテンツの問題ではなさそうです。
次に、このファイルをLiferayがヘッドレスモジュールに使っているものと比較してみたところ、多くの相違点がありました。既にブログを修正したため、当該のエラーは見られません。平たく言えば、単純なYaml形式では、Swagger Editorの形式であってもbuildREST
コマンドで期待する結果を得ることはできません。
以下に、その違いを簡単に紹介します:
Yaml for Liferayのheadless-delivery APIでは、多くのレスポンスが「デフォルト」として使用されますが、これはREST Builderでは受け入れられず、実際のレスポンスコードを使用する必要があります。 Github上のLiferay Yamlファイルは、実際のレスポンスコードを使用します。
コンポーネントセクションの説明は引用符で括る必要はありませんが、パスセクションではそうする必要があります。
- DescriptionなどはオンラインYamlでラップできますが、REST Builderはすべてを1行まとめようとします。
- パスはクオーテーションで囲む必要があります。
- タグは異なる形式でフォーマットされます。REST Builderはオンラインバージョンではなく、
tags: ["Vitamins"]
などの形式を想定しています。 - Swaggerに表示されるURLの
/v1.0
部分は、パス定義に含めるべきではありません。
他にも私が気づいていない差異があるかもしれません。先述のようなエラーが発生する場合は、ファイルをLiferay公式のものと比較し、クオーテーションの使い方や同様のフォーマットに準じているかチェックしてみるといいでしょう。
コード生成試行その2
注:私はれを2番目の試みとしていますが、おそらくここにたどり着くまでにより多くの試みをしています。 REST Builderで何らかのエラーが発生した際には、自分ファイルをLiferayのファイルと比較し、僅かな差異があれば微調整を繰り返し、REST Builderを再度実行し、最終的にビルドエラーはなくなりました。私はこれを2番目の試みと結論づけました。
この試行錯誤の末、私のYamlファイルはLiferayのものに倣ったフォーマットとなり、無事コード生成に成功しました。
$ ../../../gradlew buildREST
成功した場合の結果:
> Task :modules:headless-vitamins-impl:buildREST
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/jaxrs/application/HeadlessVitaminsApplication.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/json/BaseJSONParser.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/http/HttpInvoker.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/pagination/Page.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/pagination/Pagination.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/function/UnsafeSupplier.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/rest-openapi.yaml
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/mutation/v1_0/Mutation.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/query/v1_0/Query.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/servlet/v1_0/ServletDataImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/OpenAPIResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/dto/v1_0/Vitamin.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/dto/v1_0/Vitamin.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/serdes/v1_0/VitaminSerDes.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/dto/v1_0/Creator.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/dto/v1_0/Creator.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/serdes/v1_0/CreatorSerDes.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/BaseVitaminResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/resources/OSGI-INF/liferay/rest/v1_0/vitamin.properties
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/resource/v1_0/VitaminResource.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/VitaminResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/resource/v1_0/VitaminResource.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-test/src/testIntegration/java/com/dnebinger/headless/vitamins/resource/v1_0/test/BaseVitaminResourceTestCase.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-test/src/testIntegration/java/com/dnebinger/headless/vitamins/resource/v1_0/test/VitaminResourceTest.java
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
したがって、ここで当ブログシリーズのパート2の終わりとします。
まとめ
ブログシリーズのパート1では、新しいヘッドレスAPIのプロジェクトの作成、設定用yamlファイルの作成、オブジェクトタイプの定義およびOpenAPI yamlファイルに取り組みました。
当パートではその続きとして、RESTアプリケーションのすべてのパス(エンドポイント)を追加しました。 OpenAPI yamlファイルの作成にあたり直面するであろう一般的なポイントに触れ、buildREST
タスクエラーが発生した場合にLiferayのファイルを例として比較する方法に触れました。
そして、buildREST
を正常に呼び出して、新しいヘッドレスAPIのコードを生成することで、このパートを終了しました。
次回のパートでは、生成されたコードを掘り下げ、ロジックの追加をどこから開始する必要があるかを確認します。
それではまた!
https://github.com/dnebing/vitamins