OpenAPI Specification 系列笔记 III

OpenAPI Specification 系列笔记 III

postman 的学习差不多也到了下一个阶段了,感觉目前对 swagger 的学习也到了一种自己比较满足的情况,所以差不多改一改这个笔记就可以放出来了

上两篇笔记分别是:

https://github.com/GoldenaArcher/open-api-3.x-study

security

security 这块就是验证相关的内容了,同样比较常见的做法,是在 components → securitySchemes 中定义对应的验证,随后在 security 中调用

security 可以在 root 下和 path 下使用,分别对应全局验证以及 API 验证,下面是登陆验证和 Bearer Token 的实现:

yaml 复制代码
security:
  - BearerAuth: []
  - BasicAuth: []

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

因为 security 是在 root 下使用的,所以 swagger 默认会要求全局验证:

点击 🔒 的时候可以看到验证方法:

⚠️:我尝试了一下,只是定义 security ,不是所有的 mock server 都会自动添加验证,比如说下面使用 prism mock 后,我尝试过没有添加 bearer token 直接访问,server 也会正常返回数据。这种情况,如果要强制验证其实没啥特别好的方法,只能检查 header 中是否包含对应的 token,比如说 bearer 有 Bearer XXXX 这个格式。具体需要用户登录的验证方法,我还真没想到有什么特别好的解决方法......

API Keys

这个功能需要对接第三方的会比较熟,就是使用别人提供的 API 时,需要传输的 API key 和/或 secret,这里的 API key 也可以进行验证:

yaml 复制代码
security:
  - BearerAuth: []
  - BasicAuth: []
  - ApiKeyAuth: []

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY

实现效果如下:

OAuth 2.0

这是另一种验证方法,大概配置如下:

yaml 复制代码
security:
  - BearerAuth: []
  - BasicAuth: []
  - ApiKeyAuth: []
  - OAuth2AuthCode: [read, write, admin]

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
    OAuth2AuthCode:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: "https://example.com/oauth/authorize"
          tokenUrl: "https://example.com/oauth/token"
          scopes:
            read: Grants read access
            write: Grants write access
            admin: Grants access to admin operations

path 层使用 security

这里就是在单独的 API 级别使用 security ,这里的实现会重载 root level 的实现

下面的案例中,会在 getCategories 这里使用 security: [],这样就会放弃所有的验证,同时也能看到,🔒 在 getCategories 这里消失了:

使用 github pages host 网页

在线预览地址在:https://goldenaarcher.github.io/open-api-3.x-study/

我放弃使用私有域名啦,继续用 GH 默认的了,然后发现之前的域名被人注册了 OTL

下面包含了 index.html 的实现方式,本质上来说就是通过 CDN,导入 swagger 的 JS 和 CSS,然后在对应的锚点完成渲染即可:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1/swagger-ui.css"
    />
    <title>EazyShop API Documentation</title>
  </head>
  <body>
    <div id="eazyshop-oas"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1/swagger-ui-bundle.js"></script>
    <script>
      const ui = SwaggerUIBundle({
        url: "eazyshop.yaml",
        dom_id: "#eazyshop-oas",
        deepLinking: true,
      });
    </script>
  </body>
</html>

最终效果如下:

具体的 endpoint:

mock api

这里就使用 prism 去 mock,具体的使用方式还是很简单的,下面是 package.json

json 复制代码
{
  "name": "swagger",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "mock": "prism mock ./eazyshop.yaml -p 8080"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/GoldenaArcher/open-api-3.x-study.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/GoldenaArcher/open-api-3.x-study/issues"
  },
  "homepage": "https://github.com/GoldenaArcher/open-api-3.x-study#readme",
  "description": "",
  "dependencies": {
    "@stoplight/prism-cli": "^5.14.2"
  }
}

运行 npm run mock 后:

对应的 endpoint 效果如下:

  • 网页
  • curl

如果实现了多个 examples,可以通过 prefer 去分别 query 不同的 example:

⚠️:这个案例是另一个项目的,学 postman 的时候稍微用了一下

生成服务端模板代码(server stub)

照了下,主流的选项有两个:Swagger Codegen 和 OpenAPI Generator

前者是 SmartBear------收购 swagger 的公司做的维护,后者是社区开源维护。我现在用的版本是 OAS3.1,而 Swagger Codegen 对 OAS3.1 的支持比较差,所以案例中选用了后者

调用的方式也有几种,我这里选择使用 node cli 搞定:

bash 复制代码
❯ npm i @openapitools/openapi-generator-cli

added 146 packages, and audited 331 packages in 8s

53 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

生成对应的 stub 也就是通过 -g 控制

  • 生成 Java stub

    bash 复制代码
    ❯ npx openapi-generator-cli generate \
      -i ./eazyshop.yaml \
      -g spring \
      -o ./out/spring
    [main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
    [main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
    [main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/spring) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
    [main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: spring (server)
    [main] INFO  o.o.codegen.DefaultGenerator - Generator 'spring' is considered stable.
    [main] INFO  o.o.codegen.languages.SpringCodegen - ----------------------------------
    [main] INFO  o.o.c.languages.AbstractJavaCodegen - Environment variable JAVA_POST_PROCESS_FILE not defined so the Java code may not be properly formatted. To define it, try 'export JAVA_POST_PROCESS_FILE="/usr/local/bin/clang-format -i"' (Linux/Mac)
    [main] INFO  o.o.c.languages.AbstractJavaCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).
  • 生成 node stub

    bash 复制代码
    ❯ npx openapi-generator-cli generate \
      -i ./eazyshop.yaml \
      -g nodejs-express-server \
      -o ./out/node
    [main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
    [main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
    [main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/node) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
    [main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: nodejs-express-server (server)
    [main] INFO  o.o.codegen.DefaultGenerator - Generator 'nodejs-express-server' is considered beta.
    [main] INFO  o.o.c.l.NodeJSExpressServerCodegen - Environment variable JS_POST_PROCESS_FILE not defined so the JS code may not be properly formatted. To define it, try 'export JS_POST_PROCESS_FILE="/usr/local/bin/js-beautify -r -f"' (Linux/Mac)
    [main] INFO  o.o.c.l.NodeJSExpressServerCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_put_request. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_put_request=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_put_request=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_post_request. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_post_request=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_post_request=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_post_201_response. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_post_201_response=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_post_201_response=NewModel,ModelA=NewModelA in CLI).

生成客户端代码(Client SDK)

使用方法和生成后段代码一样,都是通过 -g 控制,不过可以添加 --additional-properties 去添加不同的集成,具体用什么框架,还是得查看官方文档

下面的案例是使用 TS+axios:

bash 复制代码
❯ npx openapi-generator-cli generate \
  -i ./eazyshop.yaml \
  -g typescript-axios \
  -o ./out/ts-axios \
  --additional-properties=supportsES6=true,useSingleRequestParameter=true,withSeparateModelsAndApi=true,apiPackage=apis,modelPackage=models,npmName=@acme/api-axios

[main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/ts-axios) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: typescript-axios (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'typescript-axios' is considered stable.
[main] INFO  o.o.c.l.AbstractTypeScriptClientCodegen - Hint: Environment variable 'TS_POST_PROCESS_FILE' (optional) not defined. E.g. to format the source code, please try 'export TS_POST_PROCESS_FILE="/usr/local/bin/prettier --write"' (Linux/Mac)
[main] INFO  o.o.c.l.AbstractTypeScriptClientCodegen - Note: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).

⚠️:这里生成的是 client SDK,不是 web client

这个生成的部分完全可以只作为 API 调用的接口

如果要实现 auto-refresh 的功能,可以在这之上进行一个包装------如封装 axios interceptor,这样可以将这部分的代码作为 API 调用的 single source of truth,也不用担心 merge conflict 的问题

额外补充

3.1 中 null 的使用

在第二篇笔记的时候我简单的提了一下 null 作为数据类型的存在,并且说明了 nullable 在 OAS3.1 中已经废弃了

这里简单的提一下 null 的具体用法

yaml 复制代码
Example:
	name:
		type: [string, 'null']

3.1 中 unevaluatedProperties

这个是对 allOf, anyOf 这种组合子进行的兜底验证,我现在也用过一两次,大概的感觉就是,用 unevaluatedProperties 会拦截没有被子组合验证过的属性,比如:

yaml 复制代码
Base:
  type: object
  properties: { name: { type: string } }

Addr:
  type: object
  properties: { address: { type: string } }

Person:
  type: object
  allOf:
    - $ref: "#/Base"
    - $ref: "#/Addr"
  unevaluatedProperties: false

{ "name": "A", "address": "X", "state": "ANY" } 这种例子,在没有 unevaluatedProperties 的情况下是一个合法的对象,但是使用了 unevaluatedProperties 就不合法,因为它包含了一个 schema 中不存在的属性

⚠️:在具体的实现过程中,我发现用 additional-properties 和 optional fields 会有一些冲突,特别是在这种 nested 的情况下,所以用 unevaluatedProperties 会更好一些

3.1 中 patternProperties

如其名,如果满足对应的 pattern 即可,经常与 unevaluatedPropertiesadditional-properties 搭配使用

3.1 中 inheritance & polymorphism

  • const 的使用

    完全相等的一个判断,规定某个值必须是指定的参数

    下面 if / then / else 里会有例子

    这个搭配新的一些语法,用起来能够组不少非常活的 combo

  • 支持 if / then / else 的语法

    语法如下:

    yaml 复制代码
    components:
      schemas:
        Pet:
          type: object
          properties:
            kind:
              type: string
              enum: [dog, cat]
            bark:
              type: boolean
            meow:
              type: boolean
          required: [kind]
          allOf:
            - if:
                properties:
                  kind: { const: dog }
              then:
                required: [bark]
              else:
                required: [meow]
          additionalProperties: false
  • unevaluatedProperties 可以对额外的性进行验证

  • dependentSchemas 的存在

    这是一个满足了 A 就必须要有 B 的存在,如下面这个例子:

    yaml 复制代码
    type: object
    properties:
      paymentMethod: { enum: [card, paypal] }
      cardNumber: { type: string }
      cardCvv: { type: string }
      paypalId: { type: string }
    
    dependentSchemas:
      paymentMethod:
        allOf:
          - if: { properties: { paymentMethod: { const: card } } }
            then: { required: [cardNumber, cardCvv] }
          - if: { properties: { paymentMethod: { const: paypal } } }
            then: { required: [paypalId] }

    这个的用法就是,如果当前的 schema 中出现了 paymentMethod,那么就要满足下面这个 allOf 的需求,如果没出现,则略过不做检查
    allOf 则是用了一个 if/then 定义了,如果是信用卡支付,则必须要卡号、cvv,如果是 paypal,则需要 paypalId,所以同时满足的是 {paymentMethod: card, cardNumber: '', cardCvv: ''}{paymentMethod: paypal, paypalId: ''} 两种格式

    上面的这个约束其实可以有其他的实现方法,但是用 dependentSchemas 的实现会更加的直观一些

  • dependentRequired ,和上面的 schema 类似,如果没有对 schema 的要求,用这个属性即可

3.0 discriminator

这是之前学习没有学到的部分------大概率是因为之前学的内容比较简单,没有太多 mapping 的需求吧

我大概学了一下,理解就是,如果要做 mapping 的话,在 3.0 就只能用 **discriminator,**以上面的支付方法为例, discriminator 的用法为:

yaml 复制代码
components:
  schemas:
    Pet:
      type: object
      required: [petType]
      properties:
        petType:
          type: string
          enum: [dog, cat]
      oneOf:
        - $ref: "#/components/schemas/Dog"
        - $ref: "#/components/schemas/Cat"
      discriminator:
        propertyName: petType
        mapping:
          dog: "#/components/schemas/Dog"
          cat: "#/components/schemas/Cat"

    Dog:
      type: object
      required: [petType, bark]
      properties:
        petType: { const: dog }
        bark: { type: boolean }

    Cat:
      type: object
      required: [petType, meow]
      properties:
        petType: { const: cat }
        meow: { type: boolean }

当然,这里还是加了一些 3.1 的新用法......

同样,如果有些对象比较大,需要分文件处理的话,这时候也可以用 discriminator 去处理,如:

yaml 复制代码
Pet:
  oneOf:
    - $ref: "./Dog.yaml"
    - $ref: "./Cat.yaml"
  discriminator:
    propertyName: petType
    mapping:
      dog: "./Dog.yaml"
      cat: "./Cat.yaml"

这样所有的定义都不需要被限制在单一的文件中

x-* 的使用

在上一篇的时候,只是简单地提了一下 **custom info,**但是实际折腾一下之后发现,这里能够折腾的东西远比单纯储藏一个 custom info 要多

下面的用法就是将两个对象存在 x-snippets 中,作为可以重复引用的参数:

需要注意的是,这种使用 snippet 的方法只能将引用值作为纯 data input 使用,而且我目前没有找到能够让 custom snippet 跨文件传输的方法,所以如果不同的文件需要用同一个引用,还是尽可能的放到 schema 或者 example 里去复用比较好

external ref

在文件 A 中引用文件 B,我这里使用了一个学习 postman 的例子,因为 endpoints 还是比较多的,所以我每个 endpoints 都分了对应的文件去实现,最后在 root 中 reference,这样可以导出一个完整的 specs 文件,具体用法如下:

需要注意的是,endpoint 中的 / 会被转译成 ~1 防止和文件路径产生冲突

在其他的 schema 中引用的方法如下:

基本上可以看到都是依赖于 $ref + 路径 + identifier 去找到对应的 reference

相关推荐
ajsbxi1 天前
【Java 基础】核心知识点梳理
java·开发语言·笔记
呱呱巨基1 天前
vim编辑器
linux·笔记·学习·编辑器·vim
新子y1 天前
【小白笔记】普通二叉树(General Binary Tree)和二叉搜索树的最近公共祖先(LCA)
开发语言·笔记·python
聪明的笨猪猪1 天前
Java JVM “调优” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
爱学习的uu1 天前
CURSOR最新使用指南及使用思路
人工智能·笔记·python·软件工程
YuCaiH1 天前
Linux文件处理
linux·笔记·嵌入式
Cathy Bryant1 天前
大模型损失函数(二):KL散度(Kullback-Leibler divergence)
笔记·神经网络·机器学习·数学建模·transformer
qq_398586541 天前
Threejs入门学习笔记
javascript·笔记·学习
hour_go1 天前
TCP/IP协议相关知识点
网络·笔记·网络协议·tcp/ip
潘达斯奈基~1 天前
在使用spark的applyInPandas方法过程中,遇到类型冲突问题如何解决
大数据·笔记