diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift similarity index 52% rename from Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift rename to Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift index 9add9482..6418def5 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift @@ -23,6 +23,23 @@ enum RawEnumBackingType { case integer } +/// The extracted enum value. +private enum EnumValue: Hashable, CustomStringConvertible { + + /// A string value. + case string(String) + + /// An integer value. + case integer(Int) + + var description: String { + switch self { + case .string(let value): return "\"\(value)\"" + case .integer(let value): return String(value) + } + } +} + extension FileTranslator { /// Returns a declaration of the specified raw value-based enum schema. @@ -42,32 +59,48 @@ extension FileTranslator { isNullable: Bool, allowedValues: [AnyCodable] ) throws -> Declaration { - let cases: [(String, LiteralDescription)] = try allowedValues.map(\.value) - .map { anyValue in - switch backingType { - case .string: - // 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 { return (context.asSwiftSafeName(""), .string("")) } + var seen: Set = [] + var cases: [(String, LiteralDescription)] = [] + func shouldAdd(_ value: EnumValue) throws -> Bool { + guard seen.insert(value).inserted else { + try diagnostics.emit( + .warning( + message: "Duplicate enum value, skipping", + context: ["value": "\(value)", "foundIn": typeName.description] + ) + ) + return false + } + return true + } + for anyValue in allowedValues.map(\.value) { + switch backingType { + case .string: + // 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 { + if try shouldAdd(.string("")) { cases.append((context.asSwiftSafeName(""), .string(""))) } + } else { guard let rawValue = anyValue as? String else { throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)") } let caseName = context.asSwiftSafeName(rawValue) - return (caseName, .string(rawValue)) - case .integer: - let rawValue: Int - if let intRawValue = anyValue as? Int { - rawValue = intRawValue - } else if let stringRawValue = anyValue as? String, let intRawValue = Int(stringRawValue) { - rawValue = intRawValue - } else { - throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)") - } - let caseName = rawValue < 0 ? "_n\(abs(rawValue))" : "_\(rawValue)" - return (caseName, .int(rawValue)) + if try shouldAdd(.string(rawValue)) { cases.append((caseName, .string(rawValue))) } + } + case .integer: + let rawValue: Int + if let intRawValue = anyValue as? Int { + rawValue = intRawValue + } else if let stringRawValue = anyValue as? String, let intRawValue = Int(stringRawValue) { + rawValue = intRawValue + } else { + throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)") } + let caseName = rawValue < 0 ? "_n\(abs(rawValue))" : "_\(rawValue)" + if try shouldAdd(.integer(rawValue)) { cases.append((caseName, .int(rawValue))) } } + } let baseConformance: String switch backingType { case .string: baseConformance = Constants.RawEnum.baseConformanceString diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index ba5e5905..05e9c70e 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1310,6 +1310,33 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasStringEnumWithDuplicates() throws { + try self.assertSchemasTranslation( + ignoredDiagnosticMessages: ["Duplicate enum value, skipping"], + """ + schemas: + MyEnum: + type: string + enum: + - one + - two + - three + - two + - four + """, + """ + public enum Schemas { + @frozen public enum MyEnum: String, Codable, Hashable, Sendable, CaseIterable { + case one = "one" + case two = "two" + case three = "three" + case four = "four" + } + } + """ + ) + } + func testComponentsSchemasIntEnum() throws { try self.assertSchemasTranslation( """