OpenAPI Suite - パート 1
コミュニティの皆さん、こんにちは。
私が作成した OpenAPI-Suite という最新のパッケージをご紹介します。これは、OpenAPI 仕様バージョン 3.0 から ObjectScript コードを生成するツールセットです。 簡単に言うと、これらのパッケージでは以下を行うことができます。
概要
OpenAPI Suite は多数のパッケージに分割されており、様々な開発者コミュニティライブラリや公開 REST サービスを使用しています。 以下の図では、開発されたすべてのパッケージと、使用されているライブラリと Web サービスを示しています。
注意: 公開 REST サービスの使用に問題がある場合は、コンバーターとバリデーターサービスの Docker インスタンスを開始することができます。
各パッケージの機能
OpenAPI Suite は、メンテナンス、改善、および今後の拡張を行いやすくするために、様々なパッケージで設計されています。 パッケージごとに役割がありますので、 それを確認してみましょう!
openapi-common-lib
これには、他のパッケージのすべての共通コードが含まれています。 たとえば、openapi-client-gen と openapi-server-gen は、OpenAPI 仕様の以下の入力を受け入れます。
ただし、%DynamicObject 内の仕様 3.0.x のみを処理できます。 変換のコードはこのパッケージにあります。 また様々なユーティリティも含まれています。
swagger-converter-cli
openapi-common-lib の依存関係です。 これは、OpenAPI バージョン 3.0 でバージョン 1.x または 2.x を変換するために公開 REST サービスの converter.swagger.io を使用する HTTP クライアントです。
swagger-validator-cli
これも openapi-common-lib の依存関係です。名前は「validator」となっていますが、仕様の検証に使用されるものではありません。 converter.swagger.io は、OpenAPI 仕様の構造を単純化できるように、「parse」サービスを提供しています。 例: 「nested object definition」の定義を作成し、それを「$ref」に置換します。 これにより、コード生成アルゴリズムで処理されるケース数が軽減されます。
openapi-client-gen
このパッケージは、開発者が REST サービスを使用しやすくするクライアントサイドのコード生成専用です。
単純な HTTP クライアントまたはプロダクションクライアント(ビジネスサービス、プロセス、オペレーション、プロダクションクラス)が含まれます。 元々、OpenAPI 2.x をサポートするために作成されていましたが、バージョン 3.x をサポートするように完全にリファクタリングされました。
openapi-server-gen
これは openapi-client-gen とは逆に、サーバーサイドのコード生成専用です。 ^%REST が存在するため、仕様バージョン 2.0 ではなく、バージョン 3.0 サポートがこのパッケージのターゲットとなっています。
openapi-suite
上記すべてのパッケージをひとまとめにし、以下を行う REST API を提供します。
この REST API を使用し、OpenAPI Suite の機能を使用するための Web インターフェースも提供されています。
ライブラリ
以下に、この開発で利用した DC 上の既存のライブラリをいくつか紹介します。
objectscript-openapi-definition
OpenAPI 仕様からモデルクラスを生成する便利なライブラリです。 これはこのプロジェクトで非常に重要な要素であり、私自身も貢献しているライブラリです。
ssl-client
SSL 構成を作成できるようにします。 主に、HTTPS リクエストの「DefaultSSL」という名前の構成の作成に使用されています。
yaml-utils
YAML 形式仕様の場合に、このライブラリは JSON 形式に変換するために使用されます。 このプロジェクトでは不可欠なライブラリです。 ちなみに、最初は openapi-client-gen バージョン 1 で YAML 仕様をテストするために開発されました。
io-redirect
これは私のライブラリの 1 つです。「write」をストリーム、ファイル、グローバル、または文字列変数にリダイレクトできます。 ログのトレースを維持するために、REST サービスで使用されています。 このコミュニティ記事からアイデアを得ました。
IPM によるインストール
このスイートをインストールするには、IPM(zpm)の使用が最適です。 多数のパッケージと依存関係があるため、IPM を使用するのが確実に便利と言えます。
zpm "install openapi-suite"; optional; zpm "install swagger-ui"
Docker によるインストール
特別なことは何もありません。このプロジェクトは intersystems-iris-dev-template を使用しています。
git clone git@github.com:lscalese/openapi-suite.git
cd openapi-suite
docker-compose up -dIris の起動にエラーがある場合は、おそらく iris-main.log の権限の問題と思われます。
以下を試してみてください。
touch iris-main.log && chmod 777 iris-main.log注意: irisowner ユーザーに RW 権限を追加するだけで十分なはずです。
使用方法
OpenAPI-Suite には、コードを「生成してダウンロード」するか「生成してインストール」するための Web インターフェースが備わっています。
このインターフェースは http://localhost:52796/openapisuite/ui/index.cspで使用できます(*必要に応じて、使用しているポート番号に変更してください)。
非常に簡単で、フォームに入力するだけです。
「Download Only」(ダウンロードのみ)ボタンをクリックした場合、コードは XML ファイルで生成されて返され、クラスはサーバーから削除されます。 生成されるクラスを一時的に保存するために使用するネームスペースは、OpenAPI-Suite がインストールされているネームスペースです(Docker インストールを使用した場合のデフォルトは IRISAPP です)。
ただし、「Install On Server」(サーバーにインストール)ボタンをクリックした場合、コードは生成・コンパイルされ、サーバーは、コード生成/コンパイルのステータス付きの JSON メッセージとログを返します。
デフォルトではこの機能は無効化されていますが、有効にするには IRIS ターミナルを開いて以下を実行してください。
Set^openapisuite.config("web","enable-install-onserver") = 1OpenAPI-Suite REST API を詳しく見る
CSP フォームは、http://localhost:52796/openapisuite にある REST サービスを使用します。
swagger-ui http://localhost:52796/swagger-ui/index.html を開き、http://localhost:52796/openapisuite/_spec を詳しく見てみましょう。
これは、Angular フレームワークを使用してより高度なフロントエンドアプリケーションを作成するための第一歩です。
コードをプログラミングで生成する
もちろん、UI の使用は必須ではありません。このセクションでは、コードをプログラミングで生成し、生成されたサービスを使用する方法について説明します。
すべてのスニペットは、dc.openapi.suite.samples.PetStore クラスでも使用できます。
HTTP クライアント
Set features("simpleHttpClientOnly") = 1Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreclient", "https://petstore3.swagger.io/api/v3/openapi.json", .features)
最初の引数は、クラスが生成されるパッケージであるため、有効なパッケージ名を渡すようにしましょう。 2 つ目の引数は、仕様を指す URL、ファイル名、ストリーム、または %DynamicObject です。 「features」は配列であり、現在以下のサブスクリプトのみを使用できます。
simpleHttpClientOnly: 1 である場合、単純な HTTP クライアントのみが生成されます。そうでない場合、プロダクションも生成されます(デフォルトの動作)。
compile: 0 である場合、生成されたコードはコンパイルされません。 エクスポートの目的のみでコードを生成する場合に便利です。 デフォルトは、compile = 1 です。
以下は、生成したばかりの HTTP クライアントで「addPet」サービスを使用する例です。
Set messageRequest = ##class(petstoreclient.requests.addPet).%New()
Set messageRequest.%ContentType = "application/json"Do messageRequest.PetNewObject().%JSONImport({"id":456,"name":"Mittens","photoUrls":["https://static.wikia.nocookie.net/disney/images/c/cb/Profile_-_Mittens.jpg/revision/latest?cb=20200709180903"],"status":"available"})
Set httpClient = ##class(petstoreclient.HttpClient).%New("https://petstore3.swagger.io/api/v3","DefaultSSL")
; MessageResponse will be an instance of petstoreclient.responses.addPetSet sc = httpClient.addPet(messageRequest, .messageResponse)
If$$$ISERR(sc) Do$SYSTEM.Status.DisplayError(sc) Quit sc
Write !,"Http Status code : ", messageResponse.httpStatusCode,!
Do messageResponse.Pet.%JSONExport()
<div class="spoiler-content" style="">
<pre class="codeblock-container" idlang="0" lang="ObjectScript" tabsize="4"><code class="language-cls hljs cos"><span class="hljs-keyword">Class</span> petstoreclient.HttpClient <span class="hljs-keyword">Extends</span> <span class="hljs-built_in">%RegisteredObject</span> [ ProcedureBlock ]
{
Parameter SERVER = "https://petstore3.swagger.io/api/v3";Parameter SSLCONFIGURATION = "DefaultSSL";Property HttpRequest [ InitialExpression = {##class(%Net.HttpRequest).%New()} ];Property SSLConfiguration As%String [ InitialExpression = {..#SSLCONFIGURATION} ];Property Server As%String [ InitialExpression = {..#SERVER} ];Property URLComponents [ MultiDimensional ]; Method %OnNew(Server As%String, SSLConfiguration As%String) As%Status { Set:$Data(Server) ..Server = Server Set:$Data(SSLConfiguration) ..SSLConfiguration = SSLConfiguration Quit..InitializeHttpRequestObject() }
Method InitializeHttpRequestObject() As%Status { Set..HttpRequest = ##class(%Net.HttpRequest).%New() Do##class(%Net.URLParser).Decompose(..Server, .components) Set:$Data(components("host"), host) ..HttpRequest.Server = host Set:$Data(components("port"), port) ..HttpRequest.Port = port Set:$$$LOWER($Get(components("scheme")))="https"..HttpRequest.Https = $$$YES, ..HttpRequest.SSLConfiguration = ..SSLConfigurationMerge:$Data(components) ..URLComponents = components Quit$$$OK }
/// Implement operationId : addPet/// post /pet Method addPet(requestMessage As petstoreclient.requests.addPet, Output responseMessage As petstoreclient.responses.addPet = {##class(petstoreclient.responses.addPet).%New()}) As%Status { Set sc = $$$OK$$$QuitOnError(requestMessage.LoadHttpRequestObject(..HttpRequest)) $$$QuitOnError(..HttpRequest.Send("POST", $Get(..URLComponents("path")) _ requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(..HttpRequest.HttpResponse, "addPet")) Quit sc } ... }
<p>
</p>
<pre class="codeblock-container" idlang="0" lang="ObjectScript" tabsize="4"><code class="language-cls hljs cos"><span class="hljs-keyword">Class</span> petstoreclient.requests.addPet <span class="hljs-keyword">Extends</span> <span class="hljs-built_in">%RegisteredObject</span> [ ProcedureBlock ]
{
Parameter METHOD = "post";Parameter URL = "/pet";Property%ConsumeAs%String;Property%ContentTypeAs%String;Property%URLAs%String [ InitialExpression = {..#URL} ];/// Use this property for body content with content-type = application/json.
/// Use this property for body content with content-type = application/xml.
/// Use this property for body content with content-type = application/x-www-form-urlencoded.Property Pet As petstoreclient.model.Pet;/// Load %Net.HttpRequest with this property object.
Method LoadHttpRequestObject(ByRef httpRequest As%Net.HttpRequest) As%Status
{
Set sc = $$$OKSet httpRequest.ContentType = ..%ContentTypeDo httpRequest.SetHeader("accept", ..%Consume)
If$Piece($$$LOWER(..%ContentType),";",1) = "application/json"Do..Pet.%JSONExportToStream(httpRequest.EntityBody)
If$Piece($$$LOWER(..%ContentType),";",1) = "application/xml"Do..Pet.XMLExportToStream(httpRequest.EntityBody)
If$Piece($$$LOWER(..%ContentType),";",1) = "application/x-www-form-urlencoded" {
; To implement. この場合、コードの生成はまだありません。$$$ThrowStatus($$$ERROR($$$NotImplemented))
}
Quit sc
}
}
<p>
</p>
<pre class="codeblock-container" idlang="0" lang="ObjectScript" tabsize="4"><code class="language-cls hljs cos"><span class="hljs-keyword">Class</span> petstoreclient.responses.addPet <span class="hljs-keyword">Extends</span> petstoreclient.responses.GenericResponse [ ProcedureBlock ]
{
/// http status code = 200 content-type = application/xml/// http status code = 200 content-type = application/json/// Property Pet As petstoreclient.model.Pet;/// Implement operationId : addPet/// post /pet Method LoadFromResponse(httpResponse As%Net.HttpResponse, caller As%String = "") As%Status { Set sc = $$$OKDo##super(httpResponse, caller) If$$$LOWER($Piece(httpResponse.ContentType,";",1))="application/xml",httpResponse.StatusCode = "200" { $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If$$$LOWER($Piece(httpResponse.ContentType,";",1))="application/json",httpResponse.StatusCode = "200" { Set..Pet = ##class(petstoreclient.model.Pet).%New() Do..Pet.%JSONImport(httpResponse.Data) Return sc } Quit sc }
}
<p>
</p>
</div>
HTTP クライアントのプロダクション
Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreproduction", "https://petstore3.swagger.io/api/v3/openapi.json")最初の引数は、単純な HTTP クライアントのコード生成をテストする場合のパッケージ名であるため、クライアントプロファクションの場合は必ず別のパッケージ名を使用してください。 2 つ目と 3 つ目も HTTP クライアントと同じルールが適用されます。
テストする前に、以下のコマンドを使用して、管理ポータルからプロダクションを起動してください。
Do##class(Ens.Director).StartProduction("petstoreproduction.Production")以下は、「addPet」サービスを使用する例ですが、今回は生成されたプロダクションを使用します。
Set messageRequest = ##class(petstoreproduction.requests.addPet).%New()
Set messageRequest.%ContentType = "application/json"Do messageRequest.PetNewObject().%JSONImport({"id":123,"name":"Kitty Galore","photoUrls":["https://www.tippett.com/wp-content/uploads/2017/01/ca2DC049.130.1264.jpg"],"status":"pending"})
; MessageResponse will be an instance of petstoreclient.responses.addPetSet sc = ##class(petstoreproduction.Utils).invokeHostSync("petstoreproduction.bp.SyncProcess", messageRequest, "petstoreproduction.bs.ProxyService", , .messageResponse)
Write !, "Take a look in visual trace (management portal)"If$$$ISERR(sc) Do$SYSTEM.Status.DisplayError(sc)
Write !,"Http Status code : ", messageResponse.httpStatusCode,!
Do messageResponse.Pet.%JSONExport()次に、視覚的なトレースを開いて詳細を確認します。

packages モデル、リクエスト、およびレスポンスに生成されたクラスは、単純な HTTP クライアント用に生成されるコードと非常によく似ています。 パッケージリクエストのクラスは Ens.Request を継承し、パッケージレスポンスのクラスは Ens.Response を継承します。 ビジネスオペレーションのデフォルトの実装は非常に単純です。以下のスニペットをご覧ください。
Class petstoreproduction.bo.Operation Extends Ens.BusinessOperation [ ProcedureBlock ]
{
Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter";Property Adapter As EnsLib.HTTP.OutboundAdapter;/// Implement operationId : addPet/// post /pet
Method addPet(requestMessage As petstoreproduction.requests.addPet, Output responseMessage As petstoreproduction.responses.addPet) As%Status
{
Set sc = $$$OK, pHttpRequestIn = ##class(%Net.HttpRequest).%New(), responseMessage = ##class(petstoreproduction.responses.addPet).%New()
$$$QuitOnError(requestMessage.LoadHttpRequestObject(pHttpRequestIn))
$$$QuitOnError(..Adapter.SendFormDataArray(.pHttpResponse, "post", pHttpRequestIn, , , ..Adapter.URL_requestMessage.%URL))
$$$QuitOnError(responseMessage.LoadFromResponse(pHttpResponse, "addPet"))
Quit sc
}
...
}
}
HTTP サーバーサイド REST アプリケーション
Set sc = ##class(dc.openapi.server.ServerAppGenerator).Generate("petstoreserver", "https://petstore3.swagger.io/api/v3/openapi.json", "/petstore/api")最初の引数は、クラスを生成するパッケージ名です。 2 つ目は HTTP クライアントと同じルールが適用されます。 3 つ目の引数は必須ではありませんが、使用されている場合、Web アプリケーションが指定された名前で作成されます(有効な Web アプリケーション名を指定することに注意してください)。
クラス petstoreserver.disp(ディスパッチ %CSP.REST クラス)は、^%REST が生成するコードに似ており、リクエストを受け入れるか拒否する多数のチェックを実行し、petstoreserver.impl で関連するサービス ClassMethod 実装を呼び出します。 主な違いは、実装メソッドに渡される引数で、これは petstoreserver.requests オブジェクトです。 例:
Class petstoreserver.disp Extends%CSP.REST [ ProcedureBlock ]
{
Parameter CHARSET = "utf-8";Parameter CONVERTINPUTSTREAM = 1;Parameter IgnoreWrites = 1;Parameter SpecificationClass = "petstoreserver.Spec";/// Process request post /petClassMethod addPet() As%Status
{
Set sc = $$$OKTry{
Set acceptedMedia = $ListFromString("application/json,application/xml,application/x-www-form-urlencoded")
If '$ListFind(acceptedMedia,$$$LOWER(%request.ContentType)) {
Do##class(%REST.Impl).%ReportRESTError(..#HTTP415UNSUPPORTEDMEDIATYPE,$$$ERROR($$$RESTContentType,%request.ContentType)) Quit
}
Do##class(%REST.Impl).%SetContentType($Get(%request.CgiEnvs("HTTP_ACCEPT")))
If '##class(%REST.Impl).%CheckAccepts("application/xml,application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) QuitIf '$isobject(%request.Content) Do##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR($$$RESTRequired,"body")) QuitSet requestMessage = ##class(petstoreserver.requests.addPet).%New()
Do requestMessage.LoadFromRequest(%request)
Set scValidateRequest = requestMessage.RequestValidate()
If$$$ISERR(scValidateRequest) Do##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR(5001,"Invalid requestMessage object.")) QuitSet response = ##class(petstoreserver.impl).addPet(requestMessage)
Do##class(petstoreserver.impl).%WriteResponse(response)
} Catch(ex) {
Do##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("petstoreserver.impl","ExposeServerExceptions"))
}
Quit sc
}
...
}
ご覧のとおり、dispatch クラスは実装メソッドを呼び出す前に「LoadFromRequest」と「RequestValidate」を呼び出しています。 これらのメソッドにはデフォルトの実装がありますが、コードジェネレーターはすべてのケースに対応できません。 現時点では、「query」、「headers」、「path」、およびコンテンツタイプが「application/json」、「application/octet-stream」、または「multipart/form-data」である body で最も一般的なケースがパラメーターとして自動的に処理されます。 開発者は、必要に応じて実装をチェック/完了する必要があります(未対応のケースについては、デフォルトでは、コードジェネレーターは $$$ThrowStatus($$$ERROR($$$NotImplemented)) を設定します)。
Class petstoreserver.requests.addPet Extends%RegisteredObject [ ProcedureBlock ]
{
Parameter METHOD = "post";Parameter URL = "/pet";Property%ConsumeAs%String;Property%ContentTypeAs%String;Property%URLAs%String [ InitialExpression = {..#URL} ];/// Use this property for body content with content-type = application/json.
/// Use this property for body content with content-type = application/xml.
/// Use this property for body content with content-type = application/x-www-form-urlencoded.Property Pet As petstoreserver.model.Pet;/// Load object properties from %CSP.Request object.
Method LoadFromRequest(request As%CSP.Request = {%request}) As%Status
{
Set sc = $$$OKSet ..%ContentType = $Piece(request.ContentType, ";", 1)
If ..%ContentType = "application/json"{
Do..PetNewObject().%JSONImport(request.Content)
}
If ..%ContentType = "application/xml" {
; To implement. There is no code generation yet for this case.$$$ThrowStatus($$$ERROR($$$NotImplemented))
}
If ..%ContentType = "application/x-www-form-urlencoded" {
; To implement. There is no code generation yet for this case.$$$ThrowStatus($$$ERROR($$$NotImplemented))
}
Quit sc
}
/// Load object properties from %CSP.Request object.
Method RequestValidate() As%Status
{
Set sc = $$$OK$$$QuitOnError(..%ValidateObject())
If ''$ListFind($ListFromString("application/json,application/xml,application/x-www-form-urlencoded"), ..%ContentType) {
Quit:..Pet=""$$$ERROR(5659, "Pet")
}
If$IsObject(..Pet) $$$QuitOnError(..Pet.%ValidateObject())
Quit sc
}
}
<p>
</p>
^%REST の使用方法と同じように、「petstoreserver.impl」クラスには、サービスに関連したすべてのメソッドが含まれており、開発者が実装する必要があります。
Class petstoreserver.impl Extends%REST.Impl [ ProcedureBlock ]
{
Parameter ExposeServerExceptions = 1;/// Service implemntation for post /petClassMethod addPet(messageRequest As petstoreserver.requests.addPet) As%Status
{
; Implement your service here.; Return {}$$$ThrowStatus($$$ERROR($$$NotImplemented))
}
...
}
生成されたパッケージの短い説明
| パケージ名 / クラス名 |
開発ステータス
OpenAPI-Suite はまだ非常に未熟な製品であり、さらに多くのテストを実施した上での改善が必要です。 OpenAPI 3 のサポートは部分的であり、さらに多くの機能がサポートされる可能性があります。
テストは公開されている仕様 https://petstore3.swagger.io/api/v3/openapi.json と比較的に単純な 2 つの仕様を使って実施されました。 もうちろん、すべてのケースに対応するには不十分です。 仕様を共有していただければ、喜んでテストに使用させていただきます。
このプロジェクトの基盤は十分であり、AsyncAPI をサポートするように拡張するなど、簡単に進化させることができると考えています。
お気軽にフィードバックをお寄せください。
このアプリケーションをご利用いただき、開発者ツールコンテストで支援していただければ幸いです。
お読みいただきありがとうございました。
