Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit YAML processing errors that Xcode can display #81

34 changes: 28 additions & 6 deletions Sources/_OpenAPIGeneratorCore/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,19 @@ public struct Diagnostic: Error, Codable {
/// A user-friendly description of the diagnostic.
public var message: String

/// The absolute path to a specific source file and optional line number within that file that triggered the diagnostic.
public struct Location: Codable {
public var filePath: String
public var lineNumber: Int?
}
public var location: Location?
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved

/// Additional information about where the issue occurred.
public var context: [String: String] = [:]

/// Creates an informative message, which doesn't represent an issue.
public static func note(message: String, context: [String: String] = [:]) -> Diagnostic {
.init(severity: .note, message: message, context: context)
public static func note(message: String, location: Location? = nil, context: [String: String] = [:]) -> Diagnostic {
.init(severity: .note, message: message, location: location, context: context)
}

/// Creates a recoverable issue, which doesn't prevent the generator
Expand All @@ -54,9 +61,10 @@ public struct Diagnostic: Error, Codable {
/// - Returns: A warning diagnostic.
public static func warning(
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
message: String,
location: Location? = nil,
context: [String: String] = [:]
) -> Diagnostic {
.init(severity: .warning, message: message, context: context)
.init(severity: .warning, message: message, location: location, context: context)
}

/// Creates a non-recoverable issue, which leads the generator to stop.
Expand All @@ -67,9 +75,10 @@ public struct Diagnostic: Error, Codable {
/// - Returns: An error diagnostic.
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
public static func error(
message: String,
location: Location? = nil,
context: [String: String] = [:]
) -> Diagnostic {
.init(severity: .error, message: message, context: context)
.init(severity: .error, message: message, location: location, context: context)
}

/// Creates a diagnostic for an unsupported feature.
Expand All @@ -85,11 +94,16 @@ public struct Diagnostic: Error, Codable {
public static func unsupported(
simonjbeaumont marked this conversation as resolved.
Show resolved Hide resolved
_ feature: String,
foundIn: String,
location: Location? = nil,
context: [String: String] = [:]
) -> Diagnostic {
var context = context
context["foundIn"] = foundIn
return warning(message: "Feature \"\(feature)\" is not supported, skipping", context: context)
return warning(
message: "Feature \"\(feature)\" is not supported, skipping",
location: location,
context: context
)
}
}

Expand All @@ -101,8 +115,16 @@ extension Diagnostic.Severity: CustomStringConvertible {

extension Diagnostic: CustomStringConvertible {
public var description: String {
var prefix = ""
if let location = location {
prefix = "\(location.filePath):"
if let line = location.lineNumber {
prefix += "\(line):"
}
prefix += " "
}
let contextString = context.map { "\($0)=\($1)" }.sorted().joined(separator: ", ")
return "\(severity): \(message) [\(contextString.isEmpty ? "" : "context: \(contextString)")]"
return "\(prefix)\(severity): \(message)\(contextString.isEmpty ? "" : " [context: \(contextString)]")"
}
}

Expand Down
93 changes: 70 additions & 23 deletions Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Yams
import OpenAPIKit30
import Foundation
import OpenAPIKit30
import Yams

/// A parser that uses the Yams library to parse the provided
/// raw file into an OpenAPI document.
Expand All @@ -30,37 +30,84 @@ struct YamsParser: ParserProtocol {
var openapi: String?
}

struct OpenAPIVersionError: Error, CustomStringConvertible, LocalizedError {
var versionString: String
var description: String {
"Unsupported document version: \(versionString). Please provide a document with OpenAPI versions in the 3.0.x set."
}
let versionedDocument: OpenAPIVersionedDocument
do {
versionedDocument = try decoder.decode(
OpenAPIVersionedDocument.self,
from: openapiData
)
} catch DecodingError.dataCorrupted(let errorContext) {
try checkParsingError(context: errorContext, input: input, diagnostics: diagnostics)
throw DecodingError.dataCorrupted(errorContext)
}

struct OpenAPIMissingVersionError: Error, CustomStringConvertible, LocalizedError {
var description: String {
"No openapi key found, please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x set."
}
}

let versionedDocument = try decoder.decode(
OpenAPIVersionedDocument.self,
from: openapiData
)

guard let openAPIVersion = versionedDocument.openapi else {
throw OpenAPIMissingVersionError()
throw Diagnostic.openAPIMissingVersionError(location: .init(filePath: input.absolutePath.path))
}
switch openAPIVersion {
case "3.0.0", "3.0.1", "3.0.2", "3.0.3":
break
default:
throw OpenAPIVersionError(versionString: "openapi: \(openAPIVersion)")
throw Diagnostic.openAPIVersionError(
versionString: "openapi: \(openAPIVersion)",
location: .init(filePath: input.absolutePath.path)
)
}

do {
return try decoder.decode(
OpenAPI.Document.self,
from: input.contents
)
} catch DecodingError.dataCorrupted(let errorContext) {
try checkParsingError(context: errorContext, input: input, diagnostics: diagnostics)
throw DecodingError.dataCorrupted(errorContext)
}
}

/// Detect specific YAML parsing errors to throw nicely formatted diagnostics for IDEs
private func checkParsingError(
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
context: DecodingError.Context,
input: InMemoryInputFile,
diagnostics: DiagnosticCollector
) throws {
if let yamlError = context.underlyingError as? YamlError {
if case .parser(let yamlContext, let yamlProblem, let yamlMark, _) = yamlError {
throw Diagnostic.error(
message: "\(yamlProblem) \(yamlContext?.description ?? "")",
location: .init(filePath: input.absolutePath.path, lineNumber: yamlMark.line - 1)
)
} else if case .scanner(let yamlContext, let yamlProblem, let yamlMark, _) = yamlError {
throw Diagnostic.error(
message: "\(yamlProblem) \(yamlContext?.description ?? "")",
location: .init(filePath: input.absolutePath.path, lineNumber: yamlMark.line - 1)
)
}
} else if let openAPIError = context.underlyingError as? OpenAPIError {
throw Diagnostic.error(
message: openAPIError.localizedDescription,
location: .init(filePath: input.absolutePath.path)
)
}
}
}

extension Diagnostic {
/// Use when the document is an unsupported version.
static func openAPIVersionError(versionString: String, location: Location) -> Diagnostic {
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
return error(
message:
"Unsupported document version: \(versionString). Please provide a document with OpenAPI versions in the 3.0.x set.",
location: location
)
}

return try decoder.decode(
OpenAPI.Document.self,
from: input.contents
// Use when the YAML document is completely missing the `openapi` version key.
static func openAPIMissingVersionError(location: Location) -> Diagnostic {
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
return error(
message:
"No openapi key found, please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x set.",
location: location
)
}
}
21 changes: 14 additions & 7 deletions Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
import _OpenAPIGeneratorCore
import ArgumentParser
import Foundation

extension _GenerateOptions {
Expand Down Expand Up @@ -65,13 +66,19 @@ extension _GenerateOptions {
- Additional imports: \(resolvedAdditionalImports.isEmpty ? "<none>" : resolvedAdditionalImports.joined(separator: ", "))
"""
)
try _Tool.runGenerator(
doc: doc,
configs: configs,
isPluginInvocation: isPluginInvocation,
outputDirectory: outputDirectory,
diagnostics: diagnostics
)
do {
try _Tool.runGenerator(
doc: doc,
configs: configs,
isPluginInvocation: isPluginInvocation,
outputDirectory: outputDirectory,
diagnostics: diagnostics
)
} catch let error as Diagnostic {
// Emit our nice Diagnostics message instead of relying on ArgumentParser output.
diagnostics.emit(error)
throw ExitCode.failure
}
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
try finalizeDiagnostics()
}
}
Loading