红鱼儿

REST easy with kbmMW #20 – OpenAPI and Swagger UI

即将推出的kbmMW更新不仅是一些bug修正,同时将包含一个新的主要功能:客户端存根生成器框架。

那什么是客户端存根生成器框架呢?

他是一个基于kbmMW smart services,可以生成由各种类型的客户端直接使用的代码,以访问基于kbmMW应用服务器的HTTP smart services。(什么是HTTP Smart Service呢?可参考洞主写的文章:kbmmw 的HTTP Smart Service入门)

 当前,kbmMW已经实现智能客户端(smart client)功能,通过这个功能,非常容易实现客户端来访问smart services中的功能。由于智能客户端依赖于后期绑定,所以开发者无法通过IDE及编译器的帮助,来获得有关参数及其类型。因此,编译器还需要生成一些代码,为IDE及编译器,来解释服务端发布的函数及方法。

上面这个问题,正是客户端存根生成器要解决的问题,但是,本文不会讨论如何为智能服务生成Delphi客户端存根,因为尽管已经做了这方面的准备工作,但这个特定功能可能不会在即将发布的版本中完整实现。相反,我将介绍利用存根生成器框架来实现另一个更复杂的代码集,满足REST世界中的典型需求。一年多前,当我咨询大公司做Java代码时,认识到了这一点。

在Java世界中,使用当时被称为Swagger的东西来记录REST接口,这是一个事实上的标准。后来它被重命名为OpenAPI,现在得到了绝大多数支持REST的开发者的认可和支持。

OpenAPI提供了REST接口的描述,可以用于文档,也可以用于为各种开发环境自动生成(存根)代码,使这些环境可以轻松利用通过REST接口发布的功能。(Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。)

您可以在此处阅读有关OpenAPI的更多信息:

https://swagger.io/

https://blog.csdn.net/sanyaoxu_2/article/details/80555328

OpenAPI不仅成为事实上的标准,还生成了可用于生成,编辑,查看和测试REST接口描述的各种工具(通常称为Swagger文件)。

其中一个工具是Swagger-UI(用户界面),它由Javascript和HTML组成,可以由Web服务器提供,为服务器中公开的REST接口提供简单易用的用户界面。

kbmMW现在完全支持所有这些。

让我们简单地开始使用Swagger-UI展示kbmMW SimpleInvocation演示服务器的REST接口是什么样的:

在左侧,可以看到公开的REST接口的OpenAPI声明,右侧可以看到用户友好的界面,通过简单的按钮点击可以调用这些界面。填充参数甚至REST方法也很容易。

如果我向右滚动到AddNumbers方法,然后单击该栏,它会打开其他信息,以及一个让我们尝试REST调用的按钮。

 

这非常酷!

那么我们如何使支持REST的应用服务器启用OpenAPI呢?

这真的很容易。

要返回REST服务的OpenAPI规范,我们只需在Unit2中向服务添加另一个REST公开方法。

     [kbmMW_Rest('method:get, path: "api", responseMimeType:"application/x-yaml"')]
     function OpenAPI:string;

这可以根据您的喜好来命名方法及其REST路径,但我们应该提供正确的responseMimeTypeOpenAPI描述的标准用YAML表示,幸运的是kbmMW完全支持。它也允许在JSON中生成OpenAPI描述,但它更像是一种与不支持YAML的系统兼容的方法。所以在这个例子中,我们在responseMimeType中指定响应类型为YAML。

// Return OpenAPI specification.
function TkbmMWCustomService2.OpenAPI:string;
begin
     // Return OpenAPI specification for all REST methods in this service
     // as YAML. Add the ASettings value: 'json:true' to return the specification
     // as JSON.
     // Add 'servers: [ "url1", "url2",.. "urln" ]' to ASettings if you want to
     // embed server location information in the specification.
     // Add 'inline:true' to inline object definitions instead of using $ref.
     // The example in the next line utilize the configuration framework to make
     // the setting easily configurable.
     Result:=TkbmMWSmartOpenAPIStubGenerator.GenerateOpenAPI('',self,'inline:$(OpenAPI.inline=false)');
end;

OpenAPI函数的实现代码非常简单,只调用OpenAPI存根生成器的GenerateOpenAPI方法,服务为“OpenAPI'ified”,并可选择设置字符串。设置字符串可以为空,在这个示例中,设置字符串包含值为:

inlinei:$(OpenAPI.inline=false)

原因是有两种有效的方法可以为REST接口生成OpenAPI规范,这些接口可以内联或通过引用获取或返回对象。

内联意味着每个对象都会详细解释它可以在REST调用的所有描述中使用的每个位置,而引用意味着在需要时描述和引用OpenAPI样式组件(对象)。引用是默认值。但是在示例代码中,我选择在kbmMW配置框架的帮助下对其进行配置。我们可以编写:inline:false或inline:true,但是样本会询问配置OpenAPI.inline值的当前值,该值可以是true或false。如果没有找到这样的值,kbmMW将使用默认值false(如=后面所示)。

也可以指定JSON是首选。这只需要将json:true添加到设置字符串,如下所示:

inline:$(OpenAPI.inline=false), json:true

显然,这也可以像内联一样进行配置。

但是我们保持原样,因此OpenAPI函数的输出将是对服务中REST方法的YAML格式化OpenAPI描述。

因此,如果我们使用浏览器打开URL:http://localhost:888/myserver/api,我们将获得完整的OpenAPI描述:

openapi: "3.0.0"
info: 
  title: SMARTDEMO
  description: "HTTP smart service supp. FastCGI"
  version: "1"
paths: 
  /myserver/api: 
    get: 
      operationId: get_myserver_api
      responses: 
        "200": 
          content: 
            application/x-yaml: 
              schema: 
                type: string
          description: "Success response"
  /myserver/helloworld: 
    get: 
      operationId: get_myserver_helloworld
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/now1: 
    get: 
      operationId: get_myserver_now1
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
                format: date-time
          description: "Success response"
  /myserver/now2: 
    get: 
      operationId: get_myserver_now2
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: number
                format: double
          description: "Success response"
  /myserver/echostring/{AString}: 
    get: 
      operationId: get_myserver_echostring__AString_
      parameters: 
        - 
          in: path
          name: AString
          required: true
          schema: 
            type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/myechostring/{AString}: 
    get: 
      operationId: get_myserver_myechostring__AString_
      parameters: 
        - 
          in: path
          name: AString
          required: true
          schema: 
            type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echourl: 
    get: 
      operationId: get_myserver_echourl
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echoheader: 
    get: 
      operationId: get_myserver_echoheader
      parameters: 
        - 
          in: header
          name: Accept
          required: true
          schema: 
            type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echoanyheader/{AHeaderName}: 
    get: 
      operationId: get_myserver_echoanyheader__AHeaderName_
      parameters: 
        - 
          in: path
          name: AHeaderName
          required: true
          schema: 
            type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echocookie: 
    get: 
      operationId: get_myserver_echocookie
      parameters: 
        - 
          in: cookie
          name: MyCookie
          required: true
          schema: 
            type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echoreversedstring: 
    post: 
      operationId: post_myserver_echoreversedstring
      requestBody: 
        required: true
        content: 
          text/plain: 
            schema: 
              type: string
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /myserver/echobytes: 
    post: 
      operationId: post_myserver_echobytes
      requestBody: 
        required: true
        content: 
          text/plain: 
            schema: 
              type: string
              format: byte
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
                format: byte
          description: "Success response"
  /myserver/echoreversedconfigstring: 
    get: 
      operationId: get_myserver_echoreversedconfigstring
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: string
          description: "Success response"
  /someabspath/addnumbers: 
    get: 
      summary: "Adds two numbers and returns result"
      operationId: add_numbers
      parameters: 
        - 
          in: query
          name: arg1
          required: true
          description: "First numeric argument"
          schema: 
            type: integer
            format: int32
        - 
          in: query
          name: arg2
          required: true
          description: "Second numeric argument"
          schema: 
            type: integer
            format: int32
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: integer
                format: int32
          description: "The result of the added numbers"
  /myserver/storeperson: 
    post: 
      operationId: post_myserver_storeperson
      requestBody: 
        required: true
        content: 
          text/plain: 
            schema: 
              "$ref": "#/components/schemas/person"
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: integer
                format: int32
          description: "Success response"
  /myserver/getperson/{id}: 
    get: 
      operationId: get_myserver_getperson__id_
      parameters: 
        - 
          in: path
          name: id
          required: true
          schema: 
            type: integer
            format: int32
      responses: 
        "200": 
          content: 
            application/json: 
              schema: 
                "$ref": "#/components/schemas/person"
          description: "Success response"
  /myserver/getpersons: 
    get: 
      operationId: get_myserver_getpersons
      responses: 
        "200": 
          content: 
            application/json: 
              schema: 
                type: array
                items: 
                  "$ref": "#/components/schemas/person"
          description: "Success response"
components: 
  schemas: 
    person: 
      properties: 
        Name: 
          type: string
        Address: 
          type: string
        Age: 
          type: integer
          format: int32
      type: object
      title: person

如果你愿意,可以随意学习。您可能会注意到有几个地方,有设施可以添加摘要和说明。例如,检查addnumbers调用:

  /someabspath/addnumbers: 
    get: 
      summary: "Adds two numbers and returns result"
      operationId: add_numbers
      parameters: 
        - 
          in: query
          name: arg1
          required: true
          description: "First numeric argument"
          schema: 
            type: integer
            format: int32
        - 
          in: query
          name: arg2
          required: true
          description: "Second numeric argument"
          schema: 
            type: integer
            format: int32
      responses: 
        "200": 
          content: 
            text/plain: 
              schema: 
                type: integer
                format: int32
          description: "The result of the added numbers"

摘要和描述来自哪里?

看一下Unit2.pas中AddNumbers函数的定义,很明显:

     // Add two numbers.
     // It can be called from regular clients, smart clients
     // and REST clients.
     // It can be called from a browser like this:
     // http://.../someabspath/addnumbers?arg1=10&arg2=20
     [kbmMW_Method]
     [kbmMW_Rest('method:get, path: "/someabspath/addnumbers", '+
                 'id:"add_numbers", '+
                 'summary:"Adds two numbers and returns result", '+
                 'resultDescription:"The result of the added numbers"')]
     function AddNumbers([kbmMW_Rest('value: "$arg1", required: true, description:"First numeric argument"')] const AValue1:integer;
                         [kbmMW_Rest('value: "$arg2", required: true, description:"Second numeric argument"')] const AValue2:integer;
                         [kbmMW_Arg(mwatRemoteLocation)] const ARemoteLocation:string):integer;

kbmMW_Rest属性中甚至有一个id值。OpenAPI要求每个REST路径必须具有唯一ID。kbmMW将自动尝试生成一个,但您可以通过id语法选择自己的ID名称,如上所示。id,summary,description和resultDescription都是(以kbmMW为单位)全部可选。如果OpenAPI需要描述性值且未给出任何值,则kbmMW提供默认值。

所以现在我们可以生成有效的OpenAPI描述。我们如何让我们的基于kbmMW的应用服务器使用Swagger-UI呈现它们

因为kbmMW可以充当Web服务器,所以这实际上也很容易。我们只需将TkbmMWFilePool实例添加到主窗体(Unit1)并将服务数据模块(Unit2)的FilePool属性设置为指向它。

 

现在,kbmMW将作为常规Web服务器工作,并在未找到要调用的REST函数时尝试提供文件。

在与SimpleInvocation服务器可执行文件相同的目录中,您应该创建一个名为MyServer的目录(以匹配服务),在其下面,我们添加一个api目录,两者都只是为了匹配Unit2中OpenAPI函数的逻辑路径。实际上,您不必使用此特定路径层次结构,但我已选择此类演示。

在api目录中,我们将放置从https://swagger.io/tools/swagger-ui/下载的文件

 

然后

您可以将dist文件夹中的所有文件逐个下载到api \ dist目录,也可以通过克隆或下载按钮下载所有文件。如果您执行稍后操作,则打开下载的zip文件并将包含内容的dist文件夹解压缩到api目录。

最后将一个名为index.html的文件添加到api目录中。您可以从以下位置复制/粘贴其内容:

<!DOCTYPE html>
<!-- HTML for static distribution bundle build -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Swagger Editor</title>
  <style>
  * {
    box-sizing: border-box;
  }
  body {
    font-family: Roboto,sans-serif;
    font-size: 9px;
    line-height: 1.42857143;
    color: #444;
    margin: 0px;
  }

  #swagger-editor {
    font-size: 1.3em;
  }

  .container {
    height: 100%;
    max-width: 880px;
    margin-left: auto;
    margin-right: auto;
  }

  #editor-wrapper {
    height: 100%;
    border:1em solid #000;
    border:none;
  }

  .Pane2 {
    overflow-y: scroll;
  }

  </style>
  <link href="./dist/swagger-editor.css" rel="stylesheet">
  <link rel="icon" type="image/png" href="./dist/favicon-32x32.png" sizes="32x32" />
  <link rel="icon" type="image/png" href="./dist/favicon-16x16.png" sizes="16x16" />
</head>

<body>
  <div id="swagger-editor"></div>
  <script src="./dist/swagger-editor-bundle.js"> </script>
  <script src="./dist/swagger-editor-standalone-preset.js"> </script>
  <script>
  window.onload = function() {
    // Build a system
    const editor = SwaggerEditorBundle({
      dom_id: '#swagger-editor',
      layout: 'StandaloneLayout',
      presets: [
        SwaggerEditorStandalonePreset
      ]
    })

    window.editor = editor
  }
  </script>

  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
    <defs>
      <symbol viewBox="0 0 20 20" id="unlocked">
            <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
      </symbol>

      <symbol viewBox="0 0 20 20" id="locked">
        <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
      </symbol>

      <symbol viewBox="0 0 20 20" id="close">
        <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
      </symbol>

      <symbol viewBox="0 0 20 20" id="large-arrow">
        <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
      </symbol>

      <symbol viewBox="0 0 20 20" id="large-arrow-down">
        <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
      </symbol>


      <symbol viewBox="0 0 24 24" id="jump-to">
        <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
      </symbol>

      <symbol viewBox="0 0 24 24" id="expand">
        <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
      </symbol>

    </defs>
  </svg>

</body>

</html>

现在你准备摇滚了。

启动服务器。然后启动浏览器并输入:

http://localhost:888/myserver/api/index.html?url=/myserver/api

这指示kbmMW提供index.html文件,该文件反过来请求生成Swagger-UI接口所需的剩余文件。最后,我们告诉Swagger-UI从/ myserver / api URL加载OpenAPI描述(如果你记得的话,将在Unit2中调用OpenAPI函数)。

您现在应该得到类似于此博客文章开头所示的视图。

在Swagger-UI界面中,您可以为kbmMW公开的所有REST功能生成服务器骨架和客户端存根。

快乐的摇摆!!!

如果你喜欢kbmMW,请分享这个词。转发博客文章,让其他人了解该产品!

https://components4developers.blog/2018/12/31/rest-easy-with-kbmmw-20-openapi/

posted on 2019-01-04 10:41  红鱼儿  阅读(676)  评论(0编辑  收藏  举报