diff --git a/Package.swift b/Package.swift index 29968d06..25151e5d 100644 --- a/Package.swift +++ b/Package.swift @@ -111,6 +111,16 @@ let package = Package( swiftSettings: swiftSettings ), + // Test Target for swift-openapi-generator + .testTarget( + name: "OpenAPIGeneratorTests", + dependencies: [ + "swift-openapi-generator", .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + resources: [.copy("Resources")], + swiftSettings: swiftSettings + ), + // Generator CLI .executableTarget( name: "swift-openapi-generator", diff --git a/Sources/_OpenAPIGeneratorCore/Diagnostics.swift b/Sources/_OpenAPIGeneratorCore/Diagnostics.swift index 1108d298..f85c991d 100644 --- a/Sources/_OpenAPIGeneratorCore/Diagnostics.swift +++ b/Sources/_OpenAPIGeneratorCore/Diagnostics.swift @@ -165,7 +165,33 @@ public protocol DiagnosticCollector { /// Submits a diagnostic to the collector. /// - Parameter diagnostic: The diagnostic to submit. - 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 { + 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 { @@ -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 @@ -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. @@ -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 @@ -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 @@ -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( _ 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 @@ -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) } } diff --git a/Sources/_OpenAPIGeneratorCore/DiagnosticsCollectorProvider.swift b/Sources/_OpenAPIGeneratorCore/DiagnosticsCollectorProvider.swift new file mode 100644 index 00000000..965eebb0 --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/DiagnosticsCollectorProvider.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Prepares a diagnostics collector. +/// - Parameter outputPath: A file path where to persist the YAML file. If `nil`, diagnostics will be printed to stderr. +/// - Returns: A tuple containing: +/// - An instance of `DiagnosticCollector` conforming to `Sendable`. +/// - A closure to finalize the diagnostics collection +public func preparedDiagnosticsCollector(outputPath: URL?) -> (any DiagnosticCollector & Sendable, () throws -> Void) { + let innerDiagnostics: any DiagnosticCollector & Sendable + let finalizeDiagnostics: () throws -> Void + + if let outputPath { + let _diagnostics = _YamlFileDiagnosticsCollector(url: outputPath) + finalizeDiagnostics = _diagnostics.finalize + innerDiagnostics = _diagnostics + } else { + innerDiagnostics = StdErrPrintingDiagnosticCollector() + finalizeDiagnostics = {} + } + let diagnostics = ErrorThrowingDiagnosticCollector(upstream: innerDiagnostics) + return (diagnostics, finalizeDiagnostics) +} diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift index e38df8d2..8f4a8cf1 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift @@ -110,7 +110,7 @@ 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 diagnostics.emit(diagnostic) } return doc } return .init( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift index a55517b4..4b48e2e1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift @@ -32,7 +32,6 @@ extension TypesFileTranslator { objectContext: JSONSchema.ObjectContext, isDeprecated: Bool ) throws -> Declaration { - let documentedProperties: [PropertyBlueprint] = try objectContext.properties .filter { key, value in @@ -42,7 +41,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 diagnostics.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.", @@ -63,7 +62,7 @@ extension TypesFileTranslator { // allowed in object properties, explicitly filter these out // here. if value.isString && value.formatString == "binary" { - diagnostics.emitUnsupportedSchema( + try diagnostics.emitUnsupportedSchema( reason: "Binary properties in object schemas.", schema: value, foundIn: foundIn diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index f7668e4f..e92b5605 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -76,7 +76,7 @@ extension TypesFileTranslator { // Attach any warnings from the parsed schema as a diagnostic. for warning in schema.warnings { - diagnostics.emit( + try diagnostics.emit( .warning( message: "Schema warning: \(warning.description)", context: [ diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift index b8df47b5..f78f5e9f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift @@ -123,7 +123,7 @@ extension FileTranslator { -> SchemaContent? { guard !map.isEmpty else { return nil } - if map.count > 1 { diagnostics.emitUnsupported("Multiple content types", foundIn: foundIn) } + if map.count > 1 { try diagnostics.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)? @@ -137,7 +137,7 @@ extension FileTranslator { contentValue ) } else { - diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn) + try diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn) chosenContent = nil } if let chosenContent { @@ -145,7 +145,7 @@ extension FileTranslator { if contentType.lowercasedType == "multipart" || contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded") { - diagnostics.emitUnsupportedIfNotNil( + try diagnostics.emitUnsupportedIfNotNil( chosenContent.content.encoding, "Custom encoding for multipart/formEncoded content", foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)" @@ -181,7 +181,7 @@ extension FileTranslator { ) throws -> SchemaContent? { let contentType = try contentKey.asGeneratorContentType if contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded") { - diagnostics.emitUnsupportedIfNotNil( + try diagnostics.emitUnsupportedIfNotNil( contentValue.encoding, "Custom encoding for formEncoded content", foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)" @@ -191,7 +191,7 @@ extension FileTranslator { if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema) } if contentType.isMultipart { guard isRequired else { - diagnostics.emit( + try diagnostics.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.", @@ -205,7 +205,7 @@ extension FileTranslator { if !excludeBinary, contentType.isBinary { return .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary))) } - diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn) + try diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn) return nil } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift index b9edc3b8..1c27dbd9 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift @@ -312,7 +312,7 @@ extension FileTranslator { } let contentType = finalContentTypeSource.contentType if finalContentTypeSource.contentType.isMultipart { - diagnostics.emitUnsupported("Multipart part cannot nest another multipart content.", foundIn: foundIn) + try diagnostics.emitUnsupported("Multipart part cannot nest another multipart content.", foundIn: foundIn) return nil } let info = MultipartPartInfo(repetition: repetitionKind, contentTypeSource: finalContentTypeSource) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index d84f9d3d..66a0ce26 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -140,7 +140,7 @@ extension FileTranslator { switch location { case .query: guard case .form = style else { - diagnostics.emitUnsupported( + try diagnostics.emitUnsupported( "Query params of style \(style.rawValue), explode: \(explode)", foundIn: foundIn ) @@ -148,14 +148,14 @@ extension FileTranslator { } case .header, .path: guard case .simple = style else { - diagnostics.emitUnsupported( + try diagnostics.emitUnsupported( "\(location.rawValue) params of style \(style.rawValue), explode: \(explode)", foundIn: foundIn ) return nil } case .cookie: - diagnostics.emitUnsupported("Cookie params", foundIn: foundIn) + try diagnostics.emitUnsupported("Cookie params", foundIn: foundIn) return nil } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index c422bec0..0176a8cd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -114,7 +114,7 @@ extension ClientFileTranslator { containerExpr = .identifierPattern(requestVariableName) supportsStyleAndExplode = true default: - diagnostics.emitUnsupported( + try diagnostics.emitUnsupported( "Parameter of type \(parameter.location.rawValue)", foundIn: parameter.description ) @@ -198,7 +198,7 @@ extension ServerFileTranslator { ]) ) default: - diagnostics.emitUnsupported( + try diagnostics.emitUnsupported( "Parameter of type \(parameter.location)", foundIn: "\(typedParameter.description)" ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift index e4735b0b..49fb4466 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift @@ -64,7 +64,7 @@ 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 diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn) return false } } @@ -82,7 +82,7 @@ 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 diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn) return false } } @@ -100,7 +100,7 @@ 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 diagnostics.emitUnsupportedSchema(reason: reason.description, schema: schema, foundIn: foundIn) return false } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift index 02921018..ce2da20d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift @@ -33,7 +33,7 @@ extension TypesFileTranslator { var decls = decls for (index, decl) in decls.enumerated() { guard let name = decl.name, boxedNames.contains(name) else { continue } - diagnostics.emit( + try diagnostics.emit( .note( message: "Detected a recursive type; it will be boxed to break the reference cycle.", context: ["name": name] diff --git a/Sources/swift-openapi-generator/YamlFileDiagnosticsCollector.swift b/Sources/_OpenAPIGeneratorCore/YamlFileDiagnosticsCollector.swift similarity index 98% rename from Sources/swift-openapi-generator/YamlFileDiagnosticsCollector.swift rename to Sources/_OpenAPIGeneratorCore/YamlFileDiagnosticsCollector.swift index 5035d7cb..17e65726 100644 --- a/Sources/swift-openapi-generator/YamlFileDiagnosticsCollector.swift +++ b/Sources/_OpenAPIGeneratorCore/YamlFileDiagnosticsCollector.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation import Yams -import _OpenAPIGeneratorCore struct _DiagnosticsYamlFileContent: Encodable { var uniqueMessages: [String] diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index 06b02d09..9f3fe83b 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -42,17 +42,7 @@ extension _GenerateOptions { featureFlags: resolvedFeatureFlags ) } - let diagnostics: any DiagnosticCollector & Sendable - let finalizeDiagnostics: () throws -> Void - if let diagnosticsOutputPath { - let _diagnostics = _YamlFileDiagnosticsCollector(url: diagnosticsOutputPath) - finalizeDiagnostics = _diagnostics.finalize - diagnostics = _diagnostics - } else { - diagnostics = StdErrPrintingDiagnosticCollector() - finalizeDiagnostics = {} - } - + let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath) let doc = self.docPath print( """ @@ -83,7 +73,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 diagnostics.emit(error) try finalizeDiagnostics() throw ExitCode.failure } catch { diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_DiagnosticsCollectorProvider.swift b/Tests/OpenAPIGeneratorCoreTests/Test_DiagnosticsCollectorProvider.swift new file mode 100644 index 00000000..d66169cb --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Test_DiagnosticsCollectorProvider.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest +@testable import _OpenAPIGeneratorCore + +final class Test_DiagnosticsCollectorProvider: XCTestCase { + + func testPreparedDiagnosticsCollectorWithOutputPath() throws { + let outputPath = URL(fileURLWithPath: "/path/to/diagnostics.yaml") + let (diagnostics, _) = preparedDiagnosticsCollector(outputPath: outputPath) + XCTAssertTrue(diagnostics is ErrorThrowingDiagnosticCollector) + + if let errorThrowingCollector = diagnostics as? ErrorThrowingDiagnosticCollector { + XCTAssertTrue(errorThrowingCollector.upstream is _YamlFileDiagnosticsCollector) + } else { + XCTFail("Expected diagnostics to be `ErrorThrowingDiagnosticCollector`") + } + } + + func testPreparedDiagnosticsCollectorWithoutOutputPath() throws { + let outputPath: URL? = nil + let (diagnostics, _) = preparedDiagnosticsCollector(outputPath: outputPath) + XCTAssertTrue(diagnostics is ErrorThrowingDiagnosticCollector) + if let errorThrowingCollector = diagnostics as? ErrorThrowingDiagnosticCollector { + XCTAssertTrue(errorThrowingCollector.upstream is StdErrPrintingDiagnosticCollector) + } else { + XCTFail("Expected diagnostics to be `ErrorThrowingDiagnosticCollector`") + } + } +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 1e9e6786..75b8be78 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -31,7 +31,7 @@ extension TestConfig { } /// Tests that the generator produces Swift files that match a reference. -class FileBasedReferenceTests: XCTestCase { +final class FileBasedReferenceTests: XCTestCase { /// Setup method called before the invocation of each test method in the class. override func setUp() { diff --git a/Tests/OpenAPIGeneratorTests/Resources/Docs/malformed-openapi.yaml b/Tests/OpenAPIGeneratorTests/Resources/Docs/malformed-openapi.yaml new file mode 100644 index 00000000..a31882d7 --- /dev/null +++ b/Tests/OpenAPIGeneratorTests/Resources/Docs/malformed-openapi.yaml @@ -0,0 +1,4 @@ +# openapi: '3.1.0' -> Missing openapi version +info: + title: MalformedDocument + version: 1.0.0 diff --git a/Tests/OpenAPIGeneratorTests/Resources/Docs/openapi-generator-config.yaml b/Tests/OpenAPIGeneratorTests/Resources/Docs/openapi-generator-config.yaml new file mode 100644 index 00000000..f68d9c85 --- /dev/null +++ b/Tests/OpenAPIGeneratorTests/Resources/Docs/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types + - server +accessModifier: internal diff --git a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift new file mode 100644 index 00000000..6d73da7e --- /dev/null +++ b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import _OpenAPIGeneratorCore +import OpenAPIKit +import ArgumentParser +@testable import swift_openapi_generator + +final class Test_GenerateOptions: XCTestCase { + + var resourcesDirectory: URL! = nil + + /// Setup method called before the invocation of each test method in the class. + override func setUpWithError() throws { + resourcesDirectory = try XCTUnwrap( + Bundle.module.url(forResource: "Resources", withExtension: nil), + "Could not find reference test resources directory." + ) + } + + func testRunGeneratorThrowsErrorDiagnostic() async throws { + let outputDirectory = URL(fileURLWithPath: "/invalid/path") + let docsDirectory = resourcesDirectory.appendingPathComponent("Docs") + let docPath = docsDirectory.appendingPathComponent("malformed-openapi.yaml") + let configPath = docsDirectory.appendingPathComponent("openapi-generator-config.yaml") + + let arguments = [docPath.path, "--config", configPath.path] + let generator = try _GenerateOptions.parse(arguments) + + do { + try await generator.runGenerator(outputDirectory: outputDirectory, pluginSource: .build, isDryRun: false) + XCTFail("Expected to throw an error, but it did not throw") + } catch let diagnostic as Diagnostic { + XCTAssertEqual(diagnostic.severity, .error, "Expected diagnostic severity to be `.error`") + } catch { XCTFail("Expected to throw a Diagnostic `.error`, but threw a different error: \(error)") } + } +}