Skip to content

Commit

Permalink
Support integer-backed enums (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
czechboy0 authored Sep 1, 2023
1 parent 074b839 commit e7cac50
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ enum EnumCaseKind: Equatable, Codable {
/// A case with a name and a raw value.
///
/// For example: `case foo = "Foo"`.
case nameWithRawValue(String)
case nameWithRawValue(LiteralDescription)

/// A case with a name and associated values.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ struct TextBasedRenderer: RendererProtocol {
case .nameOnly:
return ""
case .nameWithRawValue(let rawValue):
return " = \"\(rawValue)\""
return " = \(renderedLiteral(rawValue))"
case .nameWithAssociatedValues(let values):
if values.isEmpty {
return ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ extension FileTranslator {
members: [
.enumCase(
name: swiftName,
kind: swiftName == originalName ? .nameOnly : .nameWithRawValue(originalName)
kind: swiftName == originalName ? .nameOnly : .nameWithRawValue(.string(originalName))
)
]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension FileTranslator {
typeName: TypeName,
conformances: [String],
userDescription: String?,
cases: [(caseName: String, rawValue: String)],
cases: [(caseName: String, rawExpr: LiteralDescription)],
unknownCaseName: String?,
unknownCaseDescription: String?,
customSwitchedExpression: (Expression) -> Expression = { $0 }
Expand All @@ -40,10 +40,10 @@ extension FileTranslator {
let generateUnknownCases = unknownCaseName != nil
let knownCases: [Declaration] =
cases
.map { caseName, rawValue in
.map { caseName, rawExpr in
.enumCase(
name: caseName,
kind: generateUnknownCases ? .nameOnly : .nameWithRawValue(rawValue)
kind: generateUnknownCases ? .nameOnly : .nameWithRawValue(rawExpr)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,20 @@ extension FileTranslator {
guard let allowedValues = coreContext.allowedValues else {
throw GenericError(message: "Unexpected non-global string for \(typeName)")
}
let enumDecl = try translateStringEnum(
let enumDecl = try translateRawEnum(
backingType: .string,
typeName: typeName,
userDescription: overrides.userDescription ?? coreContext.description,
isNullable: coreContext.nullable,
allowedValues: allowedValues
)
return [enumDecl]
case let .integer(coreContext, _):
guard let allowedValues = coreContext.allowedValues else {
throw GenericError(message: "Unexpected non-global integer for \(typeName)")
}
let enumDecl = try translateRawEnum(
backingType: .integer,
typeName: typeName,
userDescription: overrides.userDescription ?? coreContext.description,
isNullable: coreContext.nullable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,66 @@
//===----------------------------------------------------------------------===//
import OpenAPIKit

/// The backing type of a raw enum.
enum RawEnumBackingType {

/// Backed by a `String`.
case string

/// Backed by an `Int`.
case integer
}

extension FileTranslator {

/// Returns a declaration of the specified string-based enum schema.
/// Returns a declaration of the specified raw value-based enum schema.
/// - Parameters:
/// - backingType: The backing type of the enum.
/// - typeName: The name of the type to give to the declared enum.
/// - userDescription: A user-specified description from the OpenAPI
/// document.
/// - isNullable: Whether the enum schema is nullable.
/// - allowedValues: The enumerated allowed values.
func translateStringEnum(
func translateRawEnum(
backingType: RawEnumBackingType,
typeName: TypeName,
userDescription: String?,
isNullable: Bool,
allowedValues: [AnyCodable]
) throws -> Declaration {
let rawValues = try allowedValues.map(\.value)
let cases: [(String, LiteralDescription)] =
try allowedValues
.map(\.value)
.map { anyValue in
// 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 ""
}
guard let string = anyValue as? String else {
throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)")
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 (swiftSafeName(for: ""), .string(""))
}
guard let rawValue = anyValue as? String else {
throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)")
}
let caseName = swiftSafeName(for: rawValue)
return (caseName, .string(rawValue))
case .integer:
guard let rawValue = anyValue as? Int else {
throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)")
}
let caseName = "_\(rawValue)"
return (caseName, .int(rawValue))
}
return string
}
let cases = rawValues.map { rawValue in
let caseName = swiftSafeName(for: rawValue)
return (caseName, rawValue)
let baseConformance: String
switch backingType {
case .string:
baseConformance = Constants.RawEnum.baseConformanceString
case .integer:
baseConformance = Constants.RawEnum.baseConformanceInteger
}
let conformances = [Constants.StringEnum.baseConformance] + Constants.StringEnum.conformances
let conformances = [baseConformance] + Constants.RawEnum.conformances
return try translateRawRepresentableEnum(
typeName: typeName,
conformances: conformances,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ extension FileTranslator {
let rawName = property.originalName
return .enumCase(
name: swiftName,
kind: swiftName == rawName ? .nameOnly : .nameWithRawValue(property.originalName)
kind: swiftName == rawName ? .nameOnly : .nameWithRawValue(.string(property.originalName))
)
}
return .enum(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,14 @@ enum Constants {
static let variableName: String = "additionalProperties"
}

/// Constants related to all generated string-based enums.
enum StringEnum {
/// Constants related to all generated raw enums.
enum RawEnum {

/// The name of the base conformance.
static let baseConformance: String = "String"
/// The name of the base conformance for string-based enums.
static let baseConformanceString: String = "String"

/// The name of the base conformance for int-based enums.
static let baseConformanceInteger: String = "Int"

/// The types that every enum conforms to.
static let conformances: [String] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ struct TypeMatcher {
typeName = .swift("Double")
}
case .integer(let core, _):
if core.allowedValues != nil {
// custom enum isn't a builtin
return nil
}
switch core.format {
case .int32:
typeName = .swift("Int32")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ extension TypesFileTranslator {
guard !contentTypes.isEmpty else {
return nil
}
let cases: [(caseName: String, rawValue: String)] =
let cases: [(caseName: String, rawExpr: LiteralDescription)] =
contentTypes
.map { contentType in
(contentSwiftName(contentType), contentType.lowercasedTypeAndSubtype)
(contentSwiftName(contentType), .string(contentType.lowercasedTypeAndSubtype))
}
return try translateRawRepresentableEnum(
typeName: acceptableContentTypeName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ final class Test_TypeMatcher: Test_Core {
.string(allowedValues: [
AnyCodable("Foo")
]),
// an int enum
.integer(allowedValues: [
AnyCodable(1)
]),

// an object with at least one property
.object(properties: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class Test_isSchemaSupported: XCTestCase {
AnyCodable("Foo")
]),

// an int enum
.integer(allowedValues: [
AnyCodable(1)
]),

// an object with at least one property
.object(properties: [
"Foo": .string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testComponentsSchemasEnum() throws {
func testComponentsSchemasStringEnum() throws {
try self.assertSchemasTranslation(
"""
schemas:
Expand All @@ -728,6 +728,29 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testComponentsSchemasIntEnum() throws {
try self.assertSchemasTranslation(
"""
schemas:
MyEnum:
type: integer
enum:
- 0
- 10
- 20
""",
"""
public enum Schemas {
@frozen public enum MyEnum: Int, Codable, Hashable, Sendable {
case _0 = 0
case _10 = 10
case _20 = 20
}
}
"""
)
}

func testComponentsSchemasEnum_open_pattern() throws {
try self.assertSchemasTranslation(
"""
Expand Down

0 comments on commit e7cac50

Please sign in to comment.