Skip to content
Merged
63 changes: 49 additions & 14 deletions Sources/_OpenAPIGeneratorCore/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,33 @@ public protocol DiagnosticCollector {

/// Submits a diagnostic to the collector.
/// - Parameter diagnostic: The diagnostic to submit.
Comment thread
czechboy0 marked this conversation as resolved.
func emit(_ diagnostic: Diagnostic)
/// - Throws: An error if the implementing type determines that one should be thrown.
func emit(_ diagnostic: Diagnostic) throws
}

/// A type that conforms to the `DiagnosticCollector` protocol.
///
/// It receives diagnostics and forwards them to an upstream `DiagnosticCollector`.
///
/// If a diagnostic with a severity of `.error` is emitted, this collector will throw the diagnostic as an error.
public struct ErrorThrowingDiagnosticCollector: DiagnosticCollector {
private let upstream: any DiagnosticCollector

/// Initializes a new `ErrorThrowingDiagnosticCollector` with an upstream `DiagnosticCollector`.
///
/// The upstream collector is where this collector will forward all received diagnostics.
///
/// - Parameter upstream: The `DiagnosticCollector` to which this collector will forward diagnostics.
public init(upstream: any DiagnosticCollector) { self.upstream = upstream }

/// Emits a diagnostic to the collector.
///
/// - Parameter diagnostic: The diagnostic to be submitted.
/// - Throws: The diagnostic itself if its severity is `.error`.
public func emit(_ diagnostic: Diagnostic) throws {
try upstream.emit(diagnostic)
if diagnostic.severity == .error { throw diagnostic }
}
}

extension DiagnosticCollector {
Expand All @@ -180,8 +206,9 @@ extension DiagnosticCollector {
/// feature was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
func emitUnsupported(_ feature: String, foundIn: String, context: [String: String] = [:]) {
emit(Diagnostic.unsupported(feature, foundIn: foundIn, context: context))
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupported(_ feature: String, foundIn: String, context: [String: String] = [:]) throws {
try emit(Diagnostic.unsupported(feature, foundIn: foundIn, context: context))
}

/// Emits a diagnostic for an unsupported schema found in the specified
Expand All @@ -193,9 +220,10 @@ extension DiagnosticCollector {
/// schema was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
func emitUnsupportedSchema(reason: String, schema: JSONSchema, foundIn: String, context: [String: String] = [:]) {
emit(Diagnostic.unsupportedSchema(reason: reason, schema: schema, foundIn: foundIn, context: context))
}
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupportedSchema(reason: String, schema: JSONSchema, foundIn: String, context: [String: String] = [:])
throws
{ try emit(Diagnostic.unsupportedSchema(reason: reason, schema: schema, foundIn: foundIn, context: context)) }

/// Emits a diagnostic for an unsupported feature found in the specified
/// type name.
Expand All @@ -206,8 +234,9 @@ extension DiagnosticCollector {
/// - foundIn: The type name related to where the issue was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
func emitUnsupported(_ feature: String, foundIn: TypeName, context: [String: String] = [:]) {
emit(Diagnostic.unsupported(feature, foundIn: foundIn.description, context: context))
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupported(_ feature: String, foundIn: TypeName, context: [String: String] = [:]) throws {
try emit(Diagnostic.unsupported(feature, foundIn: foundIn.description, context: context))
}

/// Emits a diagnostic for an unsupported feature found in the specified
Expand All @@ -222,9 +251,12 @@ extension DiagnosticCollector {
/// feature was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
func emitUnsupportedIfNotNil(_ test: Any?, _ feature: String, foundIn: String, context: [String: String] = [:]) {
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupportedIfNotNil(_ test: Any?, _ feature: String, foundIn: String, context: [String: String] = [:])
throws
{
if test == nil { return }
emitUnsupported(feature, foundIn: foundIn, context: context)
try emitUnsupported(feature, foundIn: foundIn, context: context)
}

/// Emits a diagnostic for an unsupported feature found in the specified
Expand All @@ -239,14 +271,15 @@ extension DiagnosticCollector {
/// feature was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupportedIfNotEmpty<C: Collection>(
_ test: C?,
_ feature: String,
foundIn: String,
context: [String: String] = [:]
) {
) throws {
guard let test = test, !test.isEmpty else { return }
emitUnsupported(feature, foundIn: foundIn, context: context)
try emitUnsupported(feature, foundIn: foundIn, context: context)
}

/// Emits a diagnostic for an unsupported feature found in the specified
Expand All @@ -261,9 +294,11 @@ extension DiagnosticCollector {
/// feature was detected.
/// - context: A set of key-value pairs that help the user understand
/// where the warning occurred.
func emitUnsupportedIfTrue(_ test: Bool, _ feature: String, foundIn: String, context: [String: String] = [:]) {
/// - Throws: This method will throw the diagnostic if the severity of the diagnostic is `.error`.
func emitUnsupportedIfTrue(_ test: Bool, _ feature: String, foundIn: String, context: [String: String] = [:]) throws
{
if !test { return }
emitUnsupported(feature, foundIn: foundIn, context: context)
try emitUnsupported(feature, foundIn: foundIn, context: context)
}
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ func makeGeneratorPipeline(
}
let validateDoc = { (doc: OpenAPI.Document) -> OpenAPI.Document in
let validationDiagnostics = try validator(doc, config)
for diagnostic in validationDiagnostics { diagnostics.emit(diagnostic) }
for diagnostic in validationDiagnostics {
try ErrorThrowingDiagnosticCollector(upstream: diagnostics).emit(diagnostic)
Comment thread
czechboy0 marked this conversation as resolved.
Outdated
}
return doc
}
return .init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension TypesFileTranslator {
objectContext: JSONSchema.ObjectContext,
isDeprecated: Bool
) throws -> Declaration {

let collector = ErrorThrowingDiagnosticCollector(upstream: diagnostics)
let documentedProperties: [PropertyBlueprint] = try objectContext.properties
.filter { key, value in

Expand All @@ -42,7 +42,7 @@ extension TypesFileTranslator {
// have a proper definition in the `properties` map are skipped, as they
// often imply a typo or a mistake in the document. So emit a diagnostic as well.
guard !value.inferred else {
diagnostics.emit(
try collector.emit(
.warning(
message:
"A property name only appears in the required list, but not in the properties map - this is likely a typo; skipping this property.",
Expand All @@ -63,7 +63,7 @@ extension TypesFileTranslator {
// allowed in object properties, explicitly filter these out
// here.
if value.isString && value.formatString == "binary" {
diagnostics.emitUnsupportedSchema(
try collector.emitUnsupportedSchema(
reason: "Binary properties in object schemas.",
schema: value,
foundIn: foundIn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ extension TypesFileTranslator {

// Attach any warnings from the parsed schema as a diagnostic.
for warning in schema.warnings {
diagnostics.emit(
let errorThrowing = ErrorThrowingDiagnosticCollector(upstream: diagnostics)
try errorThrowing.emit(
.warning(
message: "Schema warning: \(warning.description)",
context: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ extension FileTranslator {
-> SchemaContent?
{
guard !map.isEmpty else { return nil }
if map.count > 1 { diagnostics.emitUnsupported("Multiple content types", foundIn: foundIn) }
let collector = ErrorThrowingDiagnosticCollector(upstream: diagnostics)
if map.count > 1 { try collector.emitUnsupported("Multiple content types", foundIn: foundIn) }
let mapWithContentTypes = try map.map { key, content in try (type: key.asGeneratorContentType, value: content) }

let chosenContent: (type: ContentType, schema: SchemaContent, content: OpenAPI.Content)?
Expand All @@ -137,15 +138,15 @@ extension FileTranslator {
contentValue
)
} else {
diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn)
try collector.emitUnsupported("Unsupported content", foundIn: foundIn)
chosenContent = nil
}
if let chosenContent {
let contentType = chosenContent.type
if contentType.lowercasedType == "multipart"
|| contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded")
{
diagnostics.emitUnsupportedIfNotNil(
try collector.emitUnsupportedIfNotNil(
chosenContent.content.encoding,
"Custom encoding for multipart/formEncoded content",
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
Expand Down Expand Up @@ -180,8 +181,9 @@ extension FileTranslator {
foundIn: String
) throws -> SchemaContent? {
let contentType = try contentKey.asGeneratorContentType
let collector = ErrorThrowingDiagnosticCollector(upstream: diagnostics)
if contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded") {
diagnostics.emitUnsupportedIfNotNil(
try collector.emitUnsupportedIfNotNil(
contentValue.encoding,
"Custom encoding for formEncoded content",
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
Expand All @@ -191,7 +193,7 @@ extension FileTranslator {
if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema) }
if contentType.isMultipart {
guard isRequired else {
diagnostics.emit(
try collector.emit(
.warning(
message:
"Multipart request bodies must always be required, but found an optional one - skipping. Mark as `required: true` to get this body generated.",
Expand All @@ -205,7 +207,7 @@ extension FileTranslator {
if !excludeBinary, contentType.isBinary {
return .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary)))
}
diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn)
try collector.emitUnsupported("Unsupported content", foundIn: foundIn)
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ extension FileTranslator {
}
let contentType = finalContentTypeSource.contentType
if finalContentTypeSource.contentType.isMultipart {
diagnostics.emitUnsupported("Multipart part cannot nest another multipart content.", foundIn: foundIn)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupported("Multipart part cannot nest another multipart content.", foundIn: foundIn)
return nil
}
let info = MultipartPartInfo(repetition: repetitionKind, contentTypeSource: finalContentTypeSource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ extension FileTranslator {
}
}

let collector = ErrorThrowingDiagnosticCollector(upstream: diagnostics)
let locationTypeName = parameter.location.typeName(in: parent)
let foundIn = "\(locationTypeName.description)/\(parameter.name)"

Expand All @@ -140,22 +141,22 @@ extension FileTranslator {
switch location {
case .query:
guard case .form = style else {
diagnostics.emitUnsupported(
try collector.emitUnsupported(
"Query params of style \(style.rawValue), explode: \(explode)",
foundIn: foundIn
)
return nil
}
case .header, .path:
guard case .simple = style else {
diagnostics.emitUnsupported(
try collector.emitUnsupported(
"\(location.rawValue) params of style \(style.rawValue), explode: \(explode)",
foundIn: foundIn
)
return nil
}
case .cookie:
diagnostics.emitUnsupported("Cookie params", foundIn: foundIn)
try collector.emitUnsupported("Cookie params", foundIn: foundIn)
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,8 @@ extension ClientFileTranslator {
containerExpr = .identifierPattern(requestVariableName)
supportsStyleAndExplode = true
default:
diagnostics.emitUnsupported(
"Parameter of type \(parameter.location.rawValue)",
foundIn: parameter.description
)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupported("Parameter of type \(parameter.location.rawValue)", foundIn: parameter.description)
return nil
}
let styleAndExplodeArgs: [FunctionArgumentDescription]
Expand Down Expand Up @@ -198,10 +196,8 @@ extension ServerFileTranslator {
])
)
default:
diagnostics.emitUnsupported(
"Parameter of type \(parameter.location)",
foundIn: "\(typedParameter.description)"
)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupported("Parameter of type \(parameter.location)", foundIn: "\(typedParameter.description)")
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ extension FileTranslator {
switch try isSchemaSupported(schema, referenceStack: &referenceStack) {
case .supported: return true
case .unsupported(reason: let reason, schema: let schema):
diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
return false
}
}
Expand All @@ -82,7 +83,8 @@ extension FileTranslator {
switch try isSchemaSupported(schema, referenceStack: &referenceStack) {
case .supported: return true
case .unsupported(reason: let reason, schema: let schema):
diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
return false
}
}
Expand All @@ -100,7 +102,8 @@ extension FileTranslator {
switch try isObjectOrRefToObjectSchemaAndSupported(schema, referenceStack: &referenceStack) {
case .supported: return true
case .unsupported(reason: let reason, schema: let schema):
diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn)
return false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ extension TypesFileTranslator {
var decls = decls
for (index, decl) in decls.enumerated() {
guard let name = decl.name, boxedNames.contains(name) else { continue }
diagnostics.emit(
.note(
message: "Detected a recursive type; it will be boxed to break the reference cycle.",
context: ["name": name]
try ErrorThrowingDiagnosticCollector(upstream: diagnostics)
.emit(
.note(
message: "Detected a recursive type; it will be boxed to break the reference cycle.",
context: ["name": name]
)
)
)
decls[index] = boxedType(decl)
}
return decls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ extension _GenerateOptions {
try finalizeDiagnostics()
} catch let error as Diagnostic {
// Emit our nice Diagnostics message instead of relying on ArgumentParser output.
diagnostics.emit(error)
try ErrorThrowingDiagnosticCollector(upstream: diagnostics).emit(error)
Comment thread
czechboy0 marked this conversation as resolved.
Outdated
try finalizeDiagnostics()
throw ExitCode.failure
} catch {
Expand Down