使用 oasdiff 检测 API 中的重大变化
API 版本控制越来越普遍,而且不仅限于非常流行或商业化的 API。即使是像 Sportradar 这样的业余和特殊兴趣 API也具有多个版本。这会给开发人员和用户带来问题。
oasdiff:一个命令行和 Go 包,用于比较和检测 OpenAPI 规范中的重大变化。
版本控制往往会破坏事物。现有代码可能不会随 API 一起更新。oasdiff是一个用于检测OpenAPI规范变化的开源工具,旨在防止偏差给您的开发人员和客户带来问题。
oasdiff 可以作为 Golang 包或命令行实用程序运行。该工具比较 OpenAPI 规范(通常为 JSON 或 YAML 格式),并返回突出显示差异的报告。oasdiff 分析从端点到请求/响应参数的所有内容以进行更新和修订,特别是寻找可能导致中断集成的更改。它是CI/CD 管道的宝贵工具。
下面,我们将向您展示如何开始使用 oasdiff,以防止在更新 API 时出现任何中断或服务中断。
1.安装oasdiff
oasdiff 存储库中提到了许多安装 oasdiff 的选项,包括 macOS、Windows 和 Linux 安装程序以及使用 Go 加载。在本教程中,我们将使用 Go 方法,因为 Go 包可以轻松集成到 CI/CD 管道中。
首先,如果您还没有安装Go,您需要安装它。
安装 Go 后,在终端中输入以下命令。
go install github.com/tufin/oasdiff@latest
请注意,如果您刚刚安装了 Go,并且在运行该命令时出现错误,请打开一个新的终端实例,这应该可以解决问题。
如果您使用的是 macOS,您也可以使用以下命令使用 Brew 安装 oasdiff:
brew tap tufin/homebrew-tufin
brew install oasdiff
oasdiff 包装器
如果您想在不安装任何东西的情况下尝试一下,oasdiff 也有几个包装器。
- GitHub 动作
 - 云服务
 - OpenAPI 同步
 
2. 尝试 oasdiff
安装 oasdiff 后,您可以测试它以了解其工作原理。首先,确保您已克隆GitHub 存储库。完成后,您可以运行oasdiff diff以查看两个本地 YAML 文件之间的差异。
输入以下命令。
oasdiff diff data/openapi-test1.yaml data/openapi-test2.yaml
当你这样做时,你应该得到以下输出。
info:
    contact:
        added: true
    version:
        from: 1.0.0
        to: 1.0.1
paths:
    deleted:
        - /subscribe
        - /api/{domain}/{project}/install-command
        - /register
    modified:
        /api/{domain}/{project}/badges/security-score:
            operations:
                added:
                    - POST
                modified:
                    GET:
                        tags:
                            deleted:
                                - security
                        operationID:
                            from: GetSecurityScores
                            to: ""
                        parameters:
                            deleted:
                                cookie:
                                    - test
                                header:
                                    - user
                                    - X-Auth-Name
                            modified:
                                path:
                                    domain:
                                        schema:
                                            type:
                                                from: string
                                                to: integer
                                            format:
                                                from: hyphen-separated list
                                                to: non-negative integer
                                            description:
                                                from: Hyphen-separated list of lowercase string
                                                to: Non-negative integers (including zero)
                                            example:
                                                from: generic-bank
                                                to: "100"
                                            min:
                                                from: null
                                                to: 7
                                            pattern:
                                                from: ^(?:([a-z]+-)*([a-z]+)?)$
                                                to: ^(?:\d+)$
                                query:
                                    filter:
                                        content:
                                            mediaTypeModified:
                                                application/json:
                                                    schema:
                                                        properties:
                                                            modified:
                                                                color:
                                                                    type:
                                                                        from: string
                                                                        to: number
                                    image:
                                        explode:
                                            from: null
                                            to: true
                                        schema:
                                            description:
                                                from: alphanumeric
                                                to: alphanumeric with underscore, dash, period, slash and colon
                                        examples:
                                            deleted:
                                                - "0"
                                    token:
                                        schema:
                                            anyOf:
                                                added:
                                                    - RevisionSchema[0]
                                                    - RevisionSchema[1]
                                            type:
                                                from: string
                                                to: ""
                                            format:
                                                from: uuid
                                                to: ""
                                            description:
                                                from: RFC 4122 UUID
                                                to: ""
                                            example:
                                                from: 26734565-dbcc-449a-a370-0beaaf04b0e8
                                                to: null
                                            maxLength:
                                                from: 29
                                                to: null
                                            pattern:
                                                from: ^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12})$
                                                to: ""
                        responses:
                            added:
                                - default
                            deleted:
                                - "200"
                                - "201"
                                - "400"
            parameters:
                deleted:
                    path:
                        - domain
endpoints:
    added:
        - method: POST
          path: /api/{domain}/{project}/badges/security-score
    deleted:
        - method: POST
          path: /register
        - method: POST
          path: /subscribe
        - method: GET
          path: /api/{domain}/{project}/install-command
    modified:
        ?   method: GET
            path: /api/{domain}/{project}/badges/security-score
        :   tags:
                deleted:
                    - security
            operationID:
                from: GetSecurityScores
                to: ""
            parameters:
                deleted:
                    cookie:
                        - test
                    header:
                        - user
                        - X-Auth-Name
                modified:
                    path:
                        domain:
                            schema:
                                type:
                                    from: string
                                    to: integer
                                format:
                                    from: hyphen-separated list
                                    to: non-negative integer
                                description:
                                    from: Hyphen-separated list of lowercase string
                                    to: Non-negative integers (including zero)
                                example:
                                    from: generic-bank
                                    to: "100"
                                min:
                                    from: null
                                    to: 7
                                pattern:
                                    from: ^(?:([a-z]+-)*([a-z]+)?)$
                                    to: ^(?:\d+)$
                    query:
                        filter:
                            content:
                                mediaTypeModified:
                                    application/json:
                                        schema:
                                            properties:
                                                modified:
                                                    color:
                                                        type:
                                                            from: string
                                                            to: number
                        image:
                            explode:
                                from: null
                                to: true
                            schema:
                                description:
                                    from: alphanumeric
                                    to: alphanumeric with underscore, dash, period, slash and colon
                            examples:
                                deleted:
                                    - "0"
                        token:
                            schema:
                                anyOf:
                                    added:
                                        - RevisionSchema[0]
                                        - RevisionSchema[1]
                                type:
                                    from: string
                                    to: ""
                                format:
                                    from: uuid
                                    to: ""
                                description:
                                    from: RFC 4122 UUID
                                    to: ""
                                example:
                                    from: 26734565-dbcc-449a-a370-0beaaf04b0e8
                                    to: null
                                maxLength:
                                    from: 29
                                    to: null
                                pattern:
                                    from: ^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12})$
                                    to: ""
            responses:
                added:
                    - default
                deleted:
                    - "200"
                    - "201"
                    - "400"
security:
    deleted:
        - bearerAuth
servers:
    deleted:
        - tufin.com
tags:
    deleted:
        - security
        - reuven
externalDocs:
    deleted: true
components:
    schemas:
        deleted:
            - network-policies
            - rules
    parameters:
        deleted:
            - network-policies
    headers:
        deleted:
            - testc
            - new
            - test
    requestBodies:
        deleted:
            - reuven
    responses:
        deleted:
            - OK
    securitySchemes:
        deleted:
            - AccessToken
            - OAuth
            - bearerAuth
如您所见,oasdiff 的输出非常详细地描述了两个版本之间发生的所有修订。例如,本例中删除了以下端点:/subscribe、/api/{domain}/{project}/install-command和/register。
在该modified部分下,您可以看到端点/api/{domain}/{project}/badges/security-score:已添加 POST 命令。此外,许多功能也已被弃用,这些功能也已详细说明。
如果您想要更整洁的列表,也可以将结果返回为 HTML。不要使用-f text字符串末尾的命令,而是使用-f html,如下所示。
oasdiff diff data/openapi-test1.yaml data/openapi-test2.yaml -f html
Oasdiff 不仅适用于本地文件。您还可以使用 HTTP/s 轻松查看远程 API 之间的差异。
首先输入以下内容:
oasdiff diff https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test1.yaml https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test3.yaml -f text
结果显示,四个端点已被修改:security-score、install-command、register和subscribe。
如果您想查看两个版本之间的任何重大变化,请尝试以下步骤:
oasdiff breaking https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test1.yaml https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test3.yaml
这些结果表明,将端点中的成功状态替换为200或会导致中断。201security-score
甚至还有一个专门的命令用于评估/API路径内的端点。
oasdiff diff https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test1.yaml https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test3.yaml -f text -p "/api"
您还可以排除端点。您可以使用该–match-path命令过滤路径名,以过滤掉与特定表达式不匹配的路径。您还可以使用该–filter-extension命令过滤掉特定的扩展名。
3. 将 oasdiff 集成到 Go 项目中
使用 oasdiff 的最大原因之一是集成到自动化工作流程中。如果您使用 Go 进行开发,则可以在代码中直接使用 oasdiff。只需使用以下命令:
diff.Get(&diff.Config{}, spec1, spec2)
下面是 Go 程序中 oasdiff 的一个示例。
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
s1, err := loader.LoadFromFile("../data/simple1.yaml")
if err != nil {
    fmt.Fprintf(os.Stderr, "failed to load spec with %v", err)
    return
}
s2, err := loader.LoadFromFile("../data/simple2.yaml")
if err != nil {
    fmt.Fprintf(os.Stderr, "failed to load spec with %v", err)
    return
}
diffReport, err := diff.Get(diff.NewConfig(), s1, s2)
if err != nil {
    fmt.Fprintf(os.Stderr, "diff failed with %v", err)
    return
}
bytes, err := yaml.Marshal(diffReport)
if err != nil {
    fmt.Fprintf(os.Stderr, "failed to marshal result with %v", err)
    return
}
fmt.Printf("%s\n", bytes)
返回以下输出:
aths:
    modified:
        /api/test:
            operations:
                added:
                    - POST
                deleted:
                    - GET
endpoints:
    added:
        - method: POST
          path: /api/test
    deleted:
        - method: GET
          path: /api/test
``
您还可以使用 oasdiff 来检测代码内部的重大变化。
package main
import (
    "fmt"
    "os"
    "strings"
    "github.com/getkin/kin-openapi/openapi3"
    "github.com/tufin/oasdiff/checker"
    "github.com/tufin/oasdiff/diff"
    "github.com/tufin/oasdiff/load"
)
func main() {
    loader := openapi3.NewLoader()
    loader.IsExternalRefsAllowed = true
    s1, err := load.LoadSpecInfo(loader, load.NewSource("../data/openapi-test1.yaml"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to load spec with %v", err)
        return
    }
    s2, err := load.LoadSpecInfo(loader, load.NewSource("../data/openapi-test3.yaml"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to load spec with %v", err)
        return
    }
    diffConfig := diff.NewConfig().WithCheckBreaking()
    diffRes, operationsSources, err := diff.GetPathsDiff(diffConfig,
        []*load.SpecInfo{s1},
        []*load.SpecInfo{s2},
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "diff failed with %v", err)
        return
    }
    errs := checker.CheckBackwardCompatibility(checker.GetDefaultChecks(), diffRes, operationsSources)
    // process configuration file for ignoring errors
    errs, err = checker.ProcessIgnoredBackwardCompatibilityErrors(checker.ERR, errs, "../data/ignore-err-example.txt", checker.NewDefaultLocalizer())
    if err != nil {
        fmt.Fprintf(os.Stderr, "ignore errors failed with %v", err)
        return
    }
    // process configuration file for ignoring warnings
    errs, err = checker.ProcessIgnoredBackwardCompatibilityErrors(checker.WARN, errs, "../data/ignore-warn-example.txt", checker.NewDefaultLocalizer())
    if err != nil {
        fmt.Fprintf(os.Stderr, "ignore warnings failed with %v", err)
        return
    }
    // pretty print breaking changes errors
    if len(errs) > 0 {
        localizer := checker.NewDefaultLocalizer()
        count := errs.GetLevelCount()
        fmt.Print(localizer("total-errors", len(errs), count[checker.ERR], "error", count[checker.WARN], "warning"))
        for _, bcerr := range errs {
            fmt.Printf("%s\n\n", strings.TrimRight(bcerr.SingleLineError(localizer, checker.ColorNever), " "))
        }
    }
}
返回:
4 breaking changes: 1 error, 3 warning
error at ../data/openapi-test3.yaml, in API GET /api/{domain}/{project}/badges/security-score removed the success response with the status '201' [response-success-status-removed].
warning at ../data/openapi-test3.yaml, in API GET /api/{domain}/{project}/badges/security-score deleted the 'cookie' request parameter 'test' [request-parameter-removed].
warning at ../data/openapi-test3.yaml, in API GET /api/{domain}/{project}/badges/security-score deleted the 'header' request parameter 'user' [request-parameter-removed].
warning at ../data/openapi-test3.yaml, in API GET /api/{domain}/{project}/badges/security-score deleted the 'query' request parameter 'filter' [request-parameter-removed].
关于 oasdiff 的最终想法
随着 API 为软件行业提供更多动力,API 版本控制将变得越来越普遍。无论如何,区分两个版本只是 oasdiff 的潜在用途之一。能够检测到重大更改(尤其是在发生之前)更是将 oasdiff 集成到您的工作流程中的理由。几行代码就可以防止您和您的客户出现服务中断和意外停机。
热门API
- 1. AI文本生成
 - 2. AI图片生成_文生图
 - 3. AI图片生成_图生图
 - 4. AI图像编辑
 - 5. AI视频生成_文生视频
 - 6. AI视频生成_图生视频
 - 7. AI语音合成_文生语音
 - 8. AI文本生成(中国)
 
最新文章
- 如何实现Mock API以进行API测试 | Zuplo博客
 - 解读 TaskMatrix.AI
 - API协议设计的10种技术
 - ComfyUI API是什么:深入探索ComfyUI的API接口与应用
 - 从架构设计侧剖析: MCP vs A2A 是朋友还是对手?
 - Kimi Chat API入门指南:从注册到实现智能对话
 - 免费查询公司注册信息API的使用指南
 - 防御 API 攻击:保护您的 API 和数据的策略
 - 香港支付宝实名认证:是什么?怎么用?
 - 如何获取 Coze开放平台 API 密钥(分步指南)
 - 如何保护您的API免受自动化机器人和攻击 | Zuplo博客
 - ASP.NET Core Minimal APIs 入门指南 – JetBrains 博客