Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),

// Read OpenAPI documents
.package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.9.0"),
.package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"7.0.0"),
.package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "5.1.1"),
.package(url: "https://github.com/jpsim/Yams", "5.1.0"..<"7.0.0"),

// CLI Tool
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
Expand Down
26 changes: 25 additions & 1 deletion Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,36 @@ extension Either {
/// - Throws: An error if there's an issue looking up the value in the components.
func resolve(in components: OpenAPI.Components) throws -> B where A == OpenAPI.Reference<B> {
switch self {
case let .a(a): return try components.lookup(a)
case let .a(a): return try components.assumeLookupOnce(a)
case let .b(b): return b
}
}
}

extension OpenAPI.Components {
func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ reference: OpenAPI.Reference<ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(reference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(reference.absoluteString)
}
return result
}

func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(reference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(reference.absoluteString)
}
return result
}

func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ maybeReference: Either<OpenAPI.Reference<ReferenceType>, ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(maybeReference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(maybeReference.a?.absoluteString)
}
return result
}

}

extension JSONSchema.Schema {

/// Returns the name of the schema.
Expand Down
61 changes: 28 additions & 33 deletions Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ struct FilteredDocumentBuilder {
guard let methods = requiredEndpoints[path] else { continue }
switch pathItem {
case .a(let reference):
components.pathItems[try reference.internalComponentKey] = try document.components.lookup(reference)
components.pathItems[try reference.internalComponentKey] = try document.components.assumeLookupOnce(reference)
.filteringEndpoints { methods.contains($0.method) }
case .b(let pathItem):
filteredDocument.paths[path] = .b(pathItem.filteringEndpoints { methods.contains($0.method) })
Expand Down Expand Up @@ -187,7 +187,7 @@ struct FilteredDocumentBuilder {
/// - Parameter name: The key in the `#/components/schemas` map in the OpenAPI document.
/// - Throws: If the named schema does not exist in original OpenAPI document.
mutating func includeSchema(_ name: String) throws {
try includeSchema(.a(OpenAPI.Reference<JSONSchema>.component(named: name)))
try includeComponentsReferencedBy(.reference(.component(named: name)))
}
}

Expand Down Expand Up @@ -216,16 +216,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredPathItemReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}

mutating func includeSchema(_ maybeReference: Either<OpenAPI.Reference<JSONSchema>, JSONSchema>) throws {
switch maybeReference {
case .a(let reference):
guard requiredSchemaReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -236,7 +227,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredParameterReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -247,7 +238,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredResponseReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -256,7 +247,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredHeaderReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -265,7 +256,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredLinkReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -276,7 +267,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredCallbacksReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -287,7 +278,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredRequestReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -296,7 +287,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard requiredExampleReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -305,7 +296,7 @@ private extension FilteredDocumentBuilder {
for (path, maybePathItemReference) in document.paths {
let originalPathItem: OpenAPI.PathItem
switch maybePathItemReference {
case .a(let reference): originalPathItem = try document.components.lookup(reference)
case .a(let reference): originalPathItem = try document.components.assumeLookupOnce(reference)
case .b(let pathItem): originalPathItem = pathItem
}

Expand Down Expand Up @@ -347,7 +338,7 @@ private extension FilteredDocumentBuilder {

case .reference(let reference, _):
guard requiredSchemaReferences.insert(OpenAPI.Reference(reference)).inserted else { return }
try includeComponentsReferencedBy(document.components.lookup(reference))
try includeComponentsReferencedBy(document.components.lookupOnce(reference).flattenToJsonSchema())

case .object(_, let object):
for schema in object.properties.values { try includeComponentsReferencedBy(schema) }
Expand Down Expand Up @@ -379,20 +370,24 @@ private extension FilteredDocumentBuilder {
switch schemaContext.schema {
case .a(let reference):
guard requiredSchemaReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let schema): try includeComponentsReferencedBy(schema)
}
case .b(let contentMap):
for value in contentMap.values {
switch value.schema {
case .a(let reference):
guard requiredSchemaReferences.insert(reference).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
case .b(let schema): try includeComponentsReferencedBy(schema)
case .none: continue
}
}
for value in contentMap.values { try includeComponentsReferencedBy(value) }
}
}

mutating func includeComponentsReferencedBy(
_ contentMapEntry: Either<OpenAPI.Reference<OpenAPI.Content>, OpenAPI.Content>
) throws {
let content: OpenAPI.Content
switch contentMapEntry {
case .a(let ref): content = try document.components.assumeLookupOnce(ref)
case .b(let value): content = value
}
guard let schema = content.schema else { return }
try includeComponentsReferencedBy(schema)
}

mutating func includeComponentsReferencedBy(_ response: OpenAPI.Response) throws {
Expand All @@ -402,8 +397,8 @@ private extension FilteredDocumentBuilder {
}

mutating func includeComponentsReferencedBy(_ content: OpenAPI.Content) throws {
if let schema = content.schema { try includeSchema(schema) }
if let encoding = content.encoding {
if let schema = content.schema { try includeComponentsReferencedBy(schema) }
if let encoding = content.encodingMap {
for encoding in encoding.values {
if let headers = encoding.headers { for header in headers.values { try includeHeader(header) } }
}
Expand Down
13 changes: 1 addition & 12 deletions Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@ public struct YamsParser: ParserProtocol {
let decoder = YAMLDecoder()
let openapiData = input.contents

let decodingOptions = [
DocumentConfiguration.versionMapKey: [
// Until we move to OpenAPIKit v5.0+ we will parse OAS 3.2.0 as if it were OAS 3.1.2
"3.2.0": OpenAPI.Document.Version.v3_1_2
]
]

struct OpenAPIVersionedDocument: Decodable { var openapi: String? }

let versionedDocument: OpenAPIVersionedDocument
Expand All @@ -83,11 +76,7 @@ public struct YamsParser: ParserProtocol {
case "3.1.0", "3.1.1", "3.1.2":
document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents)
case "3.2.0":
document = try decoder.decode(
OpenAPIKit.OpenAPI.Document.self,
from: input.contents,
userInfo: decodingOptions
)
document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents)
default:
throw Diagnostic.openAPIVersionError(
versionString: "openapi: \(openAPIVersion)",
Expand Down
35 changes: 23 additions & 12 deletions Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
}

for (key, component) in doc.components.requestBodies {
let component = try doc.components.assumeLookupOnce(component)
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
Expand All @@ -81,6 +82,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
}

for (key, component) in doc.components.responses {
let component = try doc.components.assumeLookupOnce(component)
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
Expand Down Expand Up @@ -124,21 +126,30 @@ func validateReferences(in doc: ParsedOpenAPIRepresentation) throws {

func validateReferencesInContentTypes(_ content: OpenAPI.Content.Map, location: String) throws {
for (contentKey, contentType) in content {
if let reference = contentType.schema?.reference {
switch contentType {
case .a(let ref):
try validateReference(
reference,
ref,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/schema"
location: location + "/content/\(contentKey.rawValue)"
)
}
if let eitherExamples = contentType.examples?.values {
for example in eitherExamples {
if let reference = example.reference {
try validateReference(
reference,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/examples"
)
case .b(let contentType):
if let reference: JSONReference<JSONSchema> = contentType.schema?.reference {
try validateReference(
.init(reference),
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/schema"
)
}
if let eitherExamples = contentType.examples?.values {
for example in eitherExamples {
if let reference = example.reference {
try validateReference(
reference,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/examples"
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ extension FileTranslator {
// In nullable enum schemas, empty strings are parsed as Void.
// This is unlikely to be fixed, so handling that case here.
// https://github.com/apple/swift-openapi-generator/issues/118
if isNullable && anyValue is Void {
// Also handle nil values in nullable schemas.
let isNullValue = anyValue is Void || (anyValue as? String) == nil
if isNullable && isNullValue {
try addIfUnique(id: .string(""), caseName: context.safeNameGenerator.swiftMemberName(for: ""))
} else {
guard let rawValue = anyValue as? String else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ extension FileTranslator {
return try contents.compactMap { key, value in
try parseContentIfSupported(
contentKey: key,
contentValue: value,
contentValue: try components.assumeLookupOnce(value),
excludeBinary: excludeBinary,
isRequired: isRequired,
foundIn: foundIn + "/\(key.rawValue)"
Expand Down Expand Up @@ -129,12 +129,14 @@ extension FileTranslator {

let chosenContent: (type: ContentType, schema: SchemaContent, content: OpenAPI.Content)?
if let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isJSON }) {
chosenContent = (contentType, .init(contentType: contentType, schema: contentValue.schema), contentValue)
let contentValue = try components.assumeLookupOnce(contentValue)
chosenContent = (contentType, .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)), contentValue)
} else if !excludeBinary,
let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isBinary })
{
let contentValue = try components.assumeLookupOnce(contentValue)
chosenContent = (
contentType, .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary))),
contentType, .init(contentType: contentType, schema: .schema(.string(contentEncoding: .binary))),
contentValue
)
} else {
Expand Down Expand Up @@ -188,8 +190,8 @@ extension FileTranslator {
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
)
}
if contentType.isJSON { return .init(contentType: contentType, schema: contentValue.schema) }
if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema) }
if contentType.isJSON { return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)) }
if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)) }
if contentType.isMultipart {
guard isRequired else {
try diagnostics.emit(
Expand All @@ -201,10 +203,10 @@ extension FileTranslator {
)
return nil
}
return .init(contentType: contentType, schema: contentValue.schema, encoding: contentValue.encoding)
return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema), encoding: contentValue.encodingMap)
}
if !excludeBinary, contentType.isBinary {
return .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary)))
return .init(contentType: contentType, schema: .schema(.string(contentEncoding: .binary)))
}
try diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn)
return nil
Expand Down
Loading