一个包含 Swift 类型的库,这些类型可以编码和解码 OpenAPI 3.0.x 和 OpenAPI 3.1.x 文档及其组件。
你一开始最困惑的事情可以通过以下表格解释 OpenAPIKit 版本支持的 OpenAPI 规范版本。
OpenAPIKit | OpenAPI v3.0 | OpenAPI v3.1 |
---|---|---|
v2.x | ✅ | ❌ |
v3.x | ✅ | ✅ |
如果你要从 OpenAPIKit 1.x 迁移到 OpenAPIKit 2.x,请查看 v2 迁移指南。
如果你要从 OpenAPIKit 2.x 迁移到 OpenAPIKit 3.x,请查看 v3 迁移指南。
你需要明确指出要在项目中使用哪个新模块:OpenAPIKit
(现在支持 OpenAPI 规范 v3.1) 和/或 OpenAPIKit30
(像之前版本的 OpenAPIKit 一样继续支持 OpenAPI 规范 v3.0)。
在包清单中,依赖项将是以下之一
// v3.0 of spec:
dependencies: [.product(name: "OpenAPIKit30", package: "OpenAPIKit")]
// v3.1 of spec:
dependencies: [.product(name: "OpenAPIKit", package: "OpenAPIKit")]
你的导入也需要具体
// v3.0 of spec:
import OpenAPIKit30
// v3.1 of spec:
import OpenAPIKit
建议你基于 OpenAPIKit
模块构建项目,并且仅使用 OpenAPIKit30
来支持读取 OpenAPI 3.0.x 文档,然后 将它们转换 为 OpenAPI 3.1.x 文档。 此策略尚未支持的情况是你需要写出 OpenAPI 3.0.x 文档(而不是 3.1.x)。 这是一个计划中的功能,但尚未实现。 如果你的用例受益于读取 OpenAPI 3.0.x 文档并写出 OpenAPI 3.0.x 文档,那么你可以完全基于 OpenAPIKit30
模块进行操作。
大多数文档将侧重于使用 OpenAPIKit
模块和 OpenAPI 3.1.x 文档的体验。 如果你需要支持 OpenAPI 3.0.x 文档,请在深入研究此库的文档之前查看关于 支持 OpenAPI 3.0.x 文档 的部分。
你可以使用以下代码解码 JSON OpenAPI 文档(即,使用来自 Foundation 库的 JSONDecoder
)或 YAML OpenAPI 文档(即,使用来自 Yams 库的 YAMLDecoder
)
import OpenAPIKit
let decoder = ... // JSONDecoder() or YAMLDecoder()
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
你可以将从解码器返回的任何错误包装在 OpenAPI.Error
中,以从 localizedDescription
获得更友好的、人类可读的描述。
do {
try decoder.decode(OpenAPI.Document.self, from: ...)
} catch let error {
print(OpenAPI.Error(from: error).localizedDescription)
}
你可以使用以下代码编码 JSON OpenAPI 文档(即,使用来自 Foundation 库的 JSONEncoder
)或 YAML OpenAPI 文档(即,使用来自 Yams 库的 YAMLEncoder
)
let openAPIDoc: OpenAPI.Document = ...
let encoder = ... // JSONEncoder() or YAMLEncoder()
let encodedOpenAPIDoc = try encoder.encode(openAPIDoc)
由于 Swift 的类型系统,OpenAPI 规范的大部分由 OpenAPIKit 的类型表示 —— 你首先无法创建错误的 OpenAPI 文档,并且解码文档通常会因有用的错误而失败。
也就是说,你可以执行少量额外的检查来真正消除任何疑虑。
let openAPIDoc: OpenAPI.Document = ...
// perform additional validations on the document:
try openAPIDoc.validate()
你可以使用相同的验证系统来深入挖掘 OpenAPI 文档,并断言 OpenAPI 规范实际上并未强制执行的事情。 有关验证的更多信息,请参阅 OpenAPIKit 验证文档。
如果你需要处理 OpenAPI 3.0.x 文档并且只需要处理 3.0.x 文档,则可以在整个代码中使用 OpenAPIKit30
模块。
但是,如果你需要处理 OpenAPI 3.0.x 和 3.1.x 文档,建议使用 OpenAPIKit 兼容层来读取 3.0.x 文档并将其转换为 3.1.x 文档,以便你可以在程序的大部分中使用一套 Swift 类型。 以下是一个例子。
在此示例中,整个项目中只有一个文件需要导入 OpenAPIKit30
或 OpenAPIKitCompat
。 其他每个文件都只会导入 OpenAPIKit
并以 3.1.x 格式处理文档。
// import OpenAPIKit30 for OpenAPI 3.0 document support
import OpenAPIKit30
// import OpenAPIKit for OpenAPI 3.1 document support
import OpenAPIKit
// import OpenAPIKitCompat to convert between the versions
import OpenAPIKitCompat
// if most of your project just works with OpenAPI v3.1, most files only need to import OpenAPIKit.
// Only in the file where you are supporting converting from OpenAPI v3.0 to v3.1 do you need the
// other two imports.
// we can support either version by attempting to parse an old version and then a new version if the old version fails
let oldDoc: OpenAPIKit30.OpenAPI.Document?
let newDoc: OpenAPIKit.OpenAPI.Document
oldDoc = try? JSONDecoder().decode(OpenAPI.Document.self, from: someFileData)
newDoc = oldDoc?.convert(to: .v3_1_0) ??
(try! JSONDecoder().decode(OpenAPI.Document.self, from: someFileData))
// ^ Here we simply fall-back to 3.1.x if loading as 3.0.x failed. You could do a more
// graceful job of this by determining up front which version to attempt to load or by
// holding onto errors for each decode attempt so you can tell the user why the document
// failed to decode as neither 3.0.x nor 3.1.x if it fails in both cases.
Foundation 库的 JSONEncoder
和 JSONDecoder
不保证键控容器的顺序。 这意味着解码 JSON OpenAPI 文档然后再次编码可能会导致文档的各种哈希结构以不同的顺序排列。
如果保留顺序对你的用例很重要,我建议使用 Yams 和 FineJSON 库分别处理 YAML 和 JSON。 另请记住,JSON 是完全有效的 YAML,因此你也可能会从 Yams 解码 JSON 中获得良好的结果(它只是不会编码为有效的 JSON)。
Foundation JSON 编码和解码将是最稳定和经过实战考验的选择,Yams 也是一个相当成熟和稳定的选择。 FineJSON 使用较少(据我所知),但我过去曾成功使用过它。
此库使用的类型在很大程度上反映了 OpenAPI 规范 3.1.0 版本(OpenAPIKit
模块)和 3.0.3 版本(OpenAPIKit30
模块)中找到的对象定义。 项目状态 列出了规范定义的每个对象以及此库中相应类型的名称。 项目状态页面当前侧重于 OpenAPI 3.1.x,但为了确定事物的命名方式和支持的内容,你也可以主要推断 OpenAPI 3.0.x 支持的状态。
在根目录中有一个 OpenAPI.Document
。 除了适用于整个 API 的一些信息外,该文档还包含 OpenAPI.Components
(本质上是可以被 JSONReferences
和 OpenAPI.References
引用的可重用组件的字典)和一个 OpenAPI.PathItem.Map
(API 定义的路由字典)。
每个路由都是文档的 OpenAPI.PathItem.Map
中的一个条目。 此字典的键是每个路由的路径(例如 /widgets
)。 此字典的值是 OpenAPI.PathItems
,它定义给定路由支持的任何端点组合(例如 GET
、POST
、PATCH
等)。 除了在方法名称下访问路径项目上的端点(.get
、.post
等)外,还可以使用 PathItem
上的 .endpoints
方法获取匹配端点方法到操作的对数组。
路由上的每个端点都由 OpenAPI.Operation
定义。 除此之外,此操作可以指定给定端点支持的参数(路径、查询、标头等)、请求正文和响应正文/代码。
可以使用 OpenAPI 的 JSON Schema 规范派生类来非常详细地定义请求和响应正文。 此库使用 JSONSchema
类型进行此类模式定义。
基本类型 指定为 JSONSchema.integer
、JSONSchema.string
、JSONSchema.boolean
等。
模式属性 作为静态构造函数的参数给出。 默认情况下,模式是 不可为空,必需,和 通用的。 以下示例并非详尽无遗,你可以将任意数量的可选属性作为参数传递给静态构造函数。
可以使用 JSONSchema.integer(required: false)
使模式变为 可选 的(即,可以省略它),或者可以要求现有的模式提供 optionalSchemaObject()
。
可以使用 JSONSchema.number(nullable: true)
使模式变为 可为空 的,或者可以要求现有的模式提供 nullableSchemaObject()
。
可为空性突显了 OpenAPIKit 做出的一项重要决定。 JSON Schema 规范规定 OpenAPI v3.1 文档编码 可为空性状态,即可为空的属性被编码为除了它拥有的任何其他类型之外还具有 null
类型。 因此,在 OpenAPIKit 中,你将 nullability
设置为模式的属性,但是当编码/解码时,它将表示 null
是否包含在模式的 type
列表中。 如果你正在使用 OpenAPIKit30
模块,那么可为空性将按照 OpenAPI 3.0.x 规范编码为 nullable
属性。
某些类型的模式可以使用 格式 进一步专门化。 例如,JSONSchema.number(format: .double)
或 JSONSchema.string(format: .dateTime)
。
你可以使用 JSONSchema.string(allowedValues: "hello", "world")
指定模式的 允许值(例如,对于枚举类型)。
每种类型的模式都有自己的一组可以指定的附加属性。 例如,整数可以有一个 最小值:JSONSchema.integer(minimum: (0, exclusive: true))
。 在此上下文中,exclusive: true
表示该数字必须严格大于 0,而 exclusive: false
表示该数字必须大于或等于 0。
可以使用 JSONSchema.array
、JSONSchema.object
、JSONSchema.all(of:)
等构建复合对象。
例如,也许一个人由以下模式表示
JSONSchema.object(
title: "Person",
properties: [
"first_name": .string(minLength: 2),
"last_name": .string(nullable: true),
"age": .integer,
"favorite_color": .string(allowedValues: "red", "green", "blue")
]
)
有关更多信息,请查看 OpenAPIKit Schema Object 文档。
OpenAPI.Reference
类型表示 OpenAPI 规范的引用支持,它本质上只是符合 JSON 引用规范,但能够在适当的引用站点覆盖摘要和描述。
有关底层引用支持的详细信息,请参阅下一节关于 JSONReference
类型的内容。
JSONReference
类型允许您使用 OpenAPIDocuments,这些文档将其部分信息存储在共享的 Components Object 字典中,甚至是外部文件中。目前,只有所有引用都指向 Components Object 的文档才能被解引用,但您可以编码和解码所有引用。
您可以使用 JSONReference.external(URL)
创建外部引用。内部引用通常指向 Components Object 字典中的一个对象,并使用 JSONReference.component(named:)
构造。如果需要引用当前文件中的内容,但不在 Components Object 中,可以使用 JSONReference.internal(path:)
。
您可以使用 document.components.contains()
检查给定的 JSONReference
是否存在于 Components Object 中。您可以使用 document.components[reference]
访问 Components Object 中引用的对象。
您可以使用 document.components.reference(named:ofType:)
从 Components Object 创建引用。如果给定的组件在 ComponentsObject 中不存在,此方法将抛出一个错误。
您可以使用 document.components.lookup()
或 Components
类型的 subscript
将包含引用或组件的 Either
转换为该组件类型的可选值(既可以从 Either
中提取出来,也可以在 Components Object 中查找)。当找不到项目时,lookup()
方法会抛出错误,而 subscript
返回 nil
。
例如,
let apiDoc: OpenAPI.Document = ...
let addBooksPath = apiDoc.paths["/cloudloading/addBook"]
let addBooksParameters: [OpenAPI.Parameter]? = addBooksPath?.parameters.compactMap { apiDoc.components[$0] }
请注意,这会在 Components Object 中查找组件,但不会像下面在 解引用 & 解析 部分中描述的那样,将其转换为完全解引用的对象。
在 OpenAPI 规范中,安全需求(例如可以在根 Document 或 Operations 上找到)是一个字典,其中每个键是在 Components Object 中找到的安全方案的名称,每个值是适用范围的数组(当然,只有当安全方案类型是与“scopes”相关的类型时,才是一个非空数组)。
OpenAPIKit 将 SecurityRequirement
类型别名定义为一个带有 JSONReference
键的字典;这些引用指向 Components Object,并提供比 OpenAPI 规范要求的 String 值更强的约束。当然,为了保持与 OpenAPI 规范的一致性,这些被编码为 JSON/YAML 的 String 值,而不是 JSON 引用。
举个例子,假设您想通过隐式流描述 OAuth 2.0 身份验证。首先,定义一个安全方案
let oauthScheme = OpenAPI.SecurityScheme.oauth2(
flows: .init(
implicit: .init(
authorizationUrl: URL(string: "https://my-api.com/oauth2/auth")!,
scopes: ["read:widget": "read widget"]
)
)
)
接下来,将其存储在您的 OpenAPI 文档的 Components Object 中(可能还有其他条目,但在此示例中,我们只指定安全方案)
let components = OpenAPI.Components(
securitySchemes: ["implicit-oauth": oauthScheme]
)
最后,您的 OpenAPI 文档应该使用我们刚刚创建的 Components Object,并通过内部 JSON 引用引用 OAuth 隐式方案
let document = OpenAPI.Document(
info: .init(title: "API", version: "1.0"),
servers: [],
paths: [:],
components: components,
security: [[.component( named: "implicit-oauth"): ["read:widget"]]]
)
如果您的 API 支持多种替代身份验证策略(只需要使用其中一种),您的文档的 Security 数组中可能会有其他条目
let document = OpenAPI.Document(
info: .init(title: "API", version: "1.0"),
servers: [],
paths: [:],
components: components,
security: [
[.component( named: "implicit-oauth"): ["read:widget"]],
[.component( named: "auth-code-oauth"): ["read:widget"]]
]
)
许多 OpenAPIKit 类型都支持 规范扩展。正如 OpenAPI 规范中所述,这些扩展必须是对象,并且以 "x-" 为前缀。 例如,根 OpenAPI Object (OpenAPI.Document
) 上的名为 "specialProperty" 的属性是无效的,但属性 "x-specialProperty" 是有效的规范扩展。
您可以通过任何支持此功能的对象的 vendorExtensions
属性获取或设置规范扩展。键是以 "x-" 前缀开头的 Strings
,值是 AnyCodable
。 如果您设置扩展时不使用 "x-" 前缀,则在编码时将添加此前缀。
AnyCodable
可以从字面量构造,也可以显式构造。 以下都是有效的。
var document = OpenAPI.Document(...)
document.vendorExtensions["x-specialProperty1"] = true
document.vendorExtensions["x-specialProperty2"] = "hello world"
document.vendorExtensions["x-specialProperty3"] = ["hello", "world"]
document.vendorExtensions["x-specialProperty4"] = ["hello": "world"]
document.vendorExtensions["x-specialProperty5"] = AnyCodable("hello world")
除了在 Components
对象中查找内容外,您还可以完全解引用许多 OpenAPIKit 类型。 解引用类型的所有引用都已被查找(及其所有属性的引用,一直向下)。
您可以使用值的 dereferenced(in:)
方法来完全解引用它。
您甚至可以使用 OpenAPI.Document
locallyDereferenced()
方法解引用整个文档。 顾名思义,您只能解引用包含在一个文件中的整个文档(也就是说,所有引用都是“本地”的)。 具体来说,所有引用都必须位于文档的 Components Object 中。
与使用 Components
上的 lookup()
方法查找单个组件不同,解引用整个 OpenAPI.Document
将导致类型级别的更改,从而保证删除所有引用。 OpenAPI.Document
的 locallyDereferenced()
方法返回一个 DereferencedDocument
,它公开了 DereferencedPathItem
,这些 DereferencedPathItem
具有 DereferencedParameter
和 DereferencedOperation
等等。
类型原本具有引用或组件的任何地方,解引用变体都将只具有该组件。 例如,PathItem
具有一个参数数组,每个参数都是 Either<OpenAPI.Reference<Parameter>, Parameter>
,而 DereferencedPathItem
具有一个 DereferencedParameter
数组。 每种类型的解引用变体都公开了所有相同的属性,您可以通过 underlying{TypeName}
属性访问底层 OpenAPI
类型。 这使得遍历文档更加方便,因为您无需在 OpenAPI 规范允许的任何位置检查或查找引用。
对于所有解引用类型,解引用将在解引用值上存储一个新的 vendor 扩展,以跟踪该值过去引用的 Component Object 名称。 此 vendor 扩展是一个字符串值,其键为 x-component-name
。
您可以更进一步并解析文档。 在 DereferencedDocument
上调用 resolved()
将生成 OpenAPI.Document
的规范形式。 ResolvedDocument
公开的 ResolvedRoute
和 ResolvedEndpoint
将整个文档中的所有相关信息收集到它们自身中。 例如,ResolvedEndpoint
知道可以在哪些服务器上使用它,它位于什么路径,以及它支持哪些参数(即使某些参数是在 OpenAPI.Operation
中定义的,而另一些参数是在包含的 OpenAPI.PathItem
中定义的)。
如果您的最终目标是分析 OpenAPI 文档或从中生成全新的内容(例如代码),那么 ResolvedDocument
比原始的 OpenAPI.Document
更便于遍历和查询。 不利的一面是,目前不支持修改 ResolvedDocument
,然后将其转换回 OpenAPI.Document
以进行编码。
let document: OpenAPI.Document = ...
let resolvedDocument = try document
.locallyDereferenced()
.resolved()
for endpoint in resolvedDocument.endpoints {
// The description found on the PathItem containing the Operation defining this endpoint:
let routeDescription = endpoint.routeDescription
// The description found directly on the Operation defining this endpoint:
let endpointDescription = endpoint.endpointDescription
// The path, which in the OpenAPI.Document is the key of the dictionary containing
// the PathItem under which the Operation for this endpoint lives:
let path = endpoint.path
// The method, which in the OpenAPI.Document is the way you access the Operation for
// this endpoint on the PathItem (GET, PATCH, etc.):
let httpMethod = endpoint.method
// All parameters defined for the Operation _or_ the PathItem containing it:
let parameters = endpoint.parameters
// Per the specification, this is
// 1. the list of servers defined on the Operation if one is given.
// 2. the list of servers defined on the PathItem if one is given _and_
// no list was found on the Operation.
// 3. the list of servers defined on the Document if no list was found on
// the Operation _or_ the PathItem.
let servers = endpoint.servers
// and many more properties...
}
以下是一个简短的集成列表,这些集成可能立即有用,或者只是作为 OpenAPIKit 可用于利用 OpenAPI 规范的能力的示例。
如果您有想在此部分中推荐的库,请创建一个 pull request 并简要介绍您的项目。
swift-openapi-generator 在底层使用 OpenAPIKit,并生成 Swift 代码,用于与具有 OpenAPI 描述的 API 交互。
VaporOpenAPI / VaporOpenAPIExample 提供了一个从 Vapor 应用程序的路由生成 OpenAPI 的示例。
JSONAPI+OpenAPI 是一个从 JSON:API 类型生成 OpenAPI schema 的库。 该库对反向操作有一些基本的和实验性的支持,可以生成表示 OpenAPI 文档描述的 JSON:API 资源的 Swift 类型。
Swift Package Registry API 文档 使用声明性 Swift 代码和 OpenAPIKit 定义 Swift Package Registry 标准的 OpenAPI 文档。 此项目还提供了一个有用的示例,在将 OpenAPI 文档编码为 YAML 后,生成一个用户友好的 ReDoc Web 界面。
OpenAPIDiff 是一个库和一个 CLI,它实现了语义差异;也就是说,它不是简单地逐行比较两个 OpenAPI 文档的文本差异,而是解析文档并描述两个 OpenAPI AST 中的差异。
此库目前完全不支持文件读取,更不用说跟踪 $ref
到其他文件并加载它们。 您必须将 OpenAPI 文档读取到 Data
或 String
中(取决于您想使用的解码器),并且所有引用必须是同一文件的内部引用才能被解析。
但是,当您使用 Swift 类型时,此库对一些默认值持固有观点,但编码和解码与规范保持一致。 一些需要注意的关键事项:
required
是在属性上指定的,而不是在父对象上指定的(编码/解码仍然遵循 OpenAPI 规范)。JSONSchema.object(properties: [ "val": .string(required: true)])
是一个 "object" 类型,具有一个必需的 "string" 类型属性。required
在初始化时默认为 true
(同样,编码/解码仍然遵循 OpenAPI 规范)。JSONSchema.string
是一个必需的 "string" 类型。JSONSchema.string(required: false)
是一个可选的 "string" 类型。在使用此库决定使用哪个编码器/解码器之前,请参阅关于字典排序的说明。
欢迎并感谢您对 OpenAPIKit 的贡献! 该项目主要由一个人维护,这意味着额外的贡献者对完成工作的速度和数量有着巨大的影响。
请参阅贡献指南,了解关于贡献该项目的一些简要说明。
OpenAPIKit 项目非常重视代码安全性。 作为 Swift Server Workground 孵化计划的一部分,该项目遵循一套关于接收、报告和响应安全漏洞的共享标准。
请参阅安全,了解如何向 OpenAPIKit 项目报告漏洞以及报告后会发生什么。
请不要通过 GitHub issues 报告安全漏洞。
有关 OpenAPI 规范类型的完整列表,以及 OpenAPIKit 是否支持这些类型以及与 OpenAPIKit 类型的相关转换,请参阅规范覆盖率文档。 有关 OpenAPIKit 类型的详细信息,请参阅完整类型文档。