Skip to content

Commit

Permalink
Add feature flag for UUID format
Browse files Browse the repository at this point in the history
  • Loading branch information
gentges committed Sep 24, 2024
1 parent b4a1a4b commit d7dcae0
Show file tree
Hide file tree
Showing 15 changed files with 113 additions and 38 deletions.
5 changes: 5 additions & 0 deletions Sources/_OpenAPIGeneratorCore/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
public enum FeatureFlag: String, Hashable, Codable, CaseIterable, Sendable {
// needs to be here for the enum to compile
case empty

/// UUID support
///
/// Enable interpretation of `type: string, format: uuid` as `Foundation.UUID` typed data.
case uuidSupport
}

/// A set of enabled feature flags.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension TypesFileTranslator {
parent: typeName
)
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(
typeName: propertyType.typeName,
schema: schema,
Expand Down Expand Up @@ -173,7 +173,7 @@ extension TypesFileTranslator {
parent: typeName
)
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(
typeName: childType.typeName,
schema: schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ extension TypesFileTranslator {
parent: typeName
)
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(value) {
if TypeMatcher.isInlinable(value, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(
typeName: propertyType.typeName,
schema: value,
Expand Down Expand Up @@ -154,7 +154,7 @@ extension TypesFileTranslator {
components: components,
inParent: parent
)
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(
typeName: valueTypeUsage.typeName,
schema: schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ import OpenAPIKit

extension FileTranslator {
// Add helpers for reading feature flags below.

/// A boolean value indicating whether the `uuid` format on schemas should be followed.
var supportUUIDFormat: Bool {
config.featureFlags.contains(.uuidSupport)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension TypesFileTranslator {
inParent: typeName.appending(swiftComponent: nil, jsonComponent: "content")
)
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(
typeName: bodyTypeUsage.typeName,
schema: schema,
Expand Down Expand Up @@ -117,7 +117,7 @@ extension TypesFileTranslator {
schema: JSONSchema
) throws -> [Declaration] {
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(typeName: typeName, schema: schema, overrides: .none)
} else {
associatedDeclarations = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ struct TypedParameter {
/// A converted function from user-provided strings to strings
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String

/// A boolean value indicating whether the `uuid` format on schemas should be followed.
var supportUUIDFormat: Bool
}

extension TypedParameter: CustomStringConvertible {
Expand All @@ -61,19 +64,19 @@ extension TypedParameter {
/// A schema to be inlined.
///
/// - Returns: Nil when schema is referenceable.
var inlineableSchema: JSONSchema? { schema.inlineableSchema }
var inlineableSchema: JSONSchema? { schema.inlineableSchema(enableUUIDSupport: supportUUIDFormat) }
}

extension UnresolvedSchema {

/// A schema to be inlined.
///
/// - Returns: Nil when schema is referenceable.
var inlineableSchema: JSONSchema? {
func inlineableSchema(enableUUIDSupport: Bool) -> JSONSchema? {
switch self {
case .a: return nil
case let .b(schema):
if TypeMatcher.isInlinable(schema) { return schema }
if TypeMatcher.isInlinable(schema, enableUUIDSupport: enableUUIDSupport) { return schema }
return nil
}
}
Expand Down Expand Up @@ -208,7 +211,8 @@ extension FileTranslator {
explode: explode,
typeUsage: usage,
codingStrategy: codingStrategy,
asSwiftSafeName: swiftSafeName
asSwiftSafeName: swiftSafeName,
supportUUIDFormat: supportUUIDFormat
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension TypesFileTranslator {
let contentTypeName = typeName.appending(jsonComponent: "content")
let contents = requestBody.contents
for content in contents {
if TypeMatcher.isInlinable(content.content.schema) || content.content.isReferenceableMultipart {
if TypeMatcher.isInlinable(content.content.schema, enableUUIDSupport: supportUUIDFormat) || content.content.isReferenceableMultipart {
let inlineTypeDecls = try translateRequestBodyContentInTypes(content)
bodyMembers.append(contentsOf: inlineTypeDecls)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ extension TypesFileTranslator {
let associatedType = typedContent.resolvedTypeUsage
let content = typedContent.content
let schema = content.schema
if TypeMatcher.isInlinable(schema) || content.isReferenceableMultipart {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) || content.isReferenceableMultipart {
let decls: [Declaration]
if contentType.isMultipart {
decls = try translateMultipartBody(typedContent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension TypesFileTranslator {
let schema = header.schema
let typeUsage = header.typeUsage
let associatedDeclarations: [Declaration]
if TypeMatcher.isInlinable(schema) {
if TypeMatcher.isInlinable(schema, enableUUIDSupport: supportUUIDFormat) {
associatedDeclarations = try translateSchema(typeName: typeUsage.typeName, schema: schema, overrides: .none)
} else {
associatedDeclarations = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ struct TypeAssigner {
/// A converted function from user-provided strings to strings
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String

/// A boolean value indicating whether the `uuid` format on schemas should be followed.
var enableUUIDSupport: Bool

/// Returns a type name for an OpenAPI-named component type.
///
Expand Down Expand Up @@ -328,7 +331,7 @@ struct TypeAssigner {
inParent parent: TypeName,
subtype: SubtypeNamingMethod
) throws -> TypeUsage {
let typeMatcher = TypeMatcher(asSwiftSafeName: asSwiftSafeName)
let typeMatcher = TypeMatcher(asSwiftSafeName: asSwiftSafeName, enableUUIDSupport: enableUUIDSupport)
// Check if this type can be simply referenced without
// creating a new inline type.
if let referenceableType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) {
Expand Down Expand Up @@ -545,10 +548,14 @@ struct TypeAssigner {
extension FileTranslator {
/// A configured type assigner.
var typeAssigner: TypeAssigner { TypeAssigner(asSwiftSafeName: swiftSafeName) }
var typeAssigner: TypeAssigner {
TypeAssigner(asSwiftSafeName: swiftSafeName, enableUUIDSupport: supportUUIDFormat)
}
/// A configured type matcher.
var typeMatcher: TypeMatcher { TypeMatcher(asSwiftSafeName: swiftSafeName) }
var typeMatcher: TypeMatcher {
TypeMatcher(asSwiftSafeName: swiftSafeName, enableUUIDSupport: supportUUIDFormat)
}
}
/// An error used during the parsing of JSON references specified in an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ struct TypeMatcher {
/// A converted function from user-provided strings to strings
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String

/// Indication whether the uuid format should be respected.
var enableUUIDSupport: Bool

/// Returns the type name of a built-in type that matches the specified
/// schema.
Expand All @@ -43,7 +46,7 @@ struct TypeMatcher {
func tryMatchBuiltinType(for schema: JSONSchema.Schema) -> TypeUsage? {
Self._tryMatchRecursive(
for: schema,
test: { schema in Self._tryMatchBuiltinNonRecursive(for: schema) },
test: { schema in Self._tryMatchBuiltinNonRecursive(for: schema, enableUUIDSupport: enableUUIDSupport) },
matchedArrayHandler: { elementType, nullableItems in
nullableItems ? elementType.asOptional.asArray : elementType.asArray
},
Expand All @@ -69,9 +72,12 @@ struct TypeMatcher {
try Self._tryMatchRecursive(
for: schema.value,
test: { (schema) -> TypeUsage? in
if let builtinType = Self._tryMatchBuiltinNonRecursive(for: schema) { return builtinType }
if let builtinType = Self._tryMatchBuiltinNonRecursive(for: schema, enableUUIDSupport: enableUUIDSupport) {
return builtinType
}
guard case let .reference(ref, _) = schema else { return nil }
return try TypeAssigner(asSwiftSafeName: asSwiftSafeName).typeName(for: ref).asUsage
return try TypeAssigner(asSwiftSafeName: asSwiftSafeName, enableUUIDSupport: enableUUIDSupport)
.typeName(for: ref).asUsage
},
matchedArrayHandler: { elementType, nullableItems in
nullableItems ? elementType.asOptional.asArray : elementType.asArray
Expand All @@ -87,14 +93,16 @@ struct TypeMatcher {
/// A referenceable schema is one of:
/// - A builtin type
/// - A reference
/// - Parameter schema: The schema to match a referenceable type for.
/// - Parameters:
/// - schema: The schema to match a referenceable type for.
/// - enableUUIDSupport: Indication whether the uuid format should be respected.
/// - Returns: `true` if the schema is referenceable; `false` otherwise.
static func isReferenceable(_ schema: JSONSchema) -> Bool {
static func isReferenceable(_ schema: JSONSchema, enableUUIDSupport: Bool) -> Bool {
// This logic should be kept in sync with `tryMatchReferenceableType`.
_tryMatchRecursive(
for: schema.value,
test: { schema in
if _tryMatchBuiltinNonRecursive(for: schema) != nil { return true }
if _tryMatchBuiltinNonRecursive(for: schema, enableUUIDSupport: enableUUIDSupport) != nil { return true }
guard case .reference = schema else { return false }
return true
},
Expand All @@ -109,9 +117,11 @@ struct TypeMatcher {
/// A referenceable schema is one of:
/// - A builtin type
/// - A reference
/// - Parameter schema: The schema to match a referenceable type for.
/// - Parameters:
/// - schema: The schema to match a referenceable type for.
/// - enableUUIDSupport: Indication whether the uuid format should be respected.
/// - Returns: `true` if the schema is referenceable; `false` otherwise.
static func isReferenceable(_ schema: UnresolvedSchema?) -> Bool {
static func isReferenceable(_ schema: UnresolvedSchema?, enableUUIDSupport: Bool) -> Bool {
guard let schema else {
// fragment type is referenceable
return true
Expand All @@ -120,7 +130,7 @@ struct TypeMatcher {
case .a:
// is a reference
return true
case let .b(schema): return isReferenceable(schema)
case let .b(schema): return isReferenceable(schema, enableUUIDSupport: enableUUIDSupport)
}
}

Expand All @@ -131,9 +141,13 @@ struct TypeMatcher {
///
/// In other words, a type is inlinable if and only if it is not
/// referenceable.
/// - Parameter schema: The schema to match a referenceable type for.
/// - Parameters:
/// - schema: The schema to match a referenceable type for.
/// - enableUUIDSupport: Indication whether the uuid format should be respected.
/// - Returns: `true` if the schema is inlinable; `false` otherwise.
static func isInlinable(_ schema: JSONSchema) -> Bool { !isReferenceable(schema) }
static func isInlinable(_ schema: JSONSchema, enableUUIDSupport: Bool) -> Bool {
!isReferenceable(schema, enableUUIDSupport: enableUUIDSupport)
}

/// Returns a Boolean value that indicates whether the schema
/// needs to be defined inline.
Expand All @@ -142,9 +156,13 @@ struct TypeMatcher {
///
/// In other words, a type is inlinable if and only if it is not
/// referenceable.
/// - Parameter schema: The schema to match a referenceable type for.
/// - Parameters:
/// - schema: The schema to match a referenceable type for.
/// - enableUUIDSupport: Indication whether the uuid format should be respected.
/// - Returns: `true` if the schema is inlinable; `false` otherwise.
static func isInlinable(_ schema: UnresolvedSchema?) -> Bool { !isReferenceable(schema) }
static func isInlinable(_ schema: UnresolvedSchema?, enableUUIDSupport: Bool) -> Bool {
!isReferenceable(schema, enableUUIDSupport: enableUUIDSupport)
}

/// Return a reference to a multipart element type if the provided schema is referenceable.
/// - Parameters:
Expand Down Expand Up @@ -283,10 +301,15 @@ struct TypeMatcher {
/// - Important: Optionality from the `JSONSchema` is not applied, since
/// this function takes `JSONSchema.Schema`, and optionality is defined
/// at the `JSONSchema` level.
/// - Parameter schema: The schema to match a referenceable type for.
/// - Parameters:
/// - schema: The schema to match a referenceable type for.
/// - enableUUIDSupport: Indication whether the uuid format should be respected.
/// - Returns: A type usage for the schema if the schema is built-in.
/// Otherwise, returns nil.
private static func _tryMatchBuiltinNonRecursive(for schema: JSONSchema.Schema) -> TypeUsage? {
private static func _tryMatchBuiltinNonRecursive(
for schema: JSONSchema.Schema,
enableUUIDSupport: Bool
) -> TypeUsage? {
let typeName: TypeName
switch schema {
case .boolean(_): typeName = .swift("Bool")
Expand Down Expand Up @@ -316,7 +339,7 @@ struct TypeMatcher {
default:
switch core.format {
case .dateTime: typeName = .date
case .uuid: typeName = .uuid
case .uuid where enableUUIDSupport: typeName = .uuid
default: typeName = .string
}
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Test_Core: XCTestCase {

var typeAssigner: TypeAssigner { makeTranslator().typeAssigner }

var typeMatcher: TypeMatcher { makeTranslator().typeMatcher }
var typeMatcher: TypeMatcher { makeTranslator(featureFlags: [.uuidSupport]).typeMatcher }

var asSwiftSafeName: (String) -> String { makeTranslator().swiftSafeName }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ final class Test_TypeMatcher: Test_Core {
.fullyQualifiedSwiftName,
name
)
XCTAssertTrue(TypeMatcher.isReferenceable(schema))
XCTAssertFalse(TypeMatcher.isInlinable(schema))
XCTAssertTrue(TypeMatcher.isReferenceable(schema, enableUUIDSupport: false))
XCTAssertFalse(TypeMatcher.isInlinable(schema, enableUUIDSupport: false))
}
}

Expand All @@ -146,8 +146,8 @@ final class Test_TypeMatcher: Test_Core {
typeMatcher.tryMatchBuiltinType(for: schema.value),
"Type is expected to not match a builtin type: \(schema)"
)
XCTAssertFalse(TypeMatcher.isReferenceable(schema), "Expected schema not to be referenceable: \(schema)")
XCTAssertTrue(TypeMatcher.isInlinable(schema), "Expected schema to be inlinable: \(schema)")
XCTAssertFalse(TypeMatcher.isReferenceable(schema, enableUUIDSupport: false), "Expected schema not to be referenceable: \(schema)")
XCTAssertTrue(TypeMatcher.isInlinable(schema, enableUUIDSupport: false), "Expected schema to be inlinable: \(schema)")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class FileBasedReferenceTests: XCTestCase {
#endif
}

func testPetstore() throws { try _test(referenceProject: .init(name: .petstore)) }
func testPetstore() throws { try _test(referenceProject: .init(name: .petstore), featureFlags: [.uuidSupport]) }

// MARK: - Private

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,37 @@ final class SnippetBasedReferenceTests: XCTestCase {
"""
)
}

func testComponentsSchemasUUID() throws {
try self.assertSchemasTranslation(
featureFlags: [.uuidSupport],
"""
schemas:
MyUUID:
type: string
format: uuid
""",
"""
public enum Schemas {
public typealias MyUUID = Foundation.UUID
}
"""
)
// Without UUID support, the schema will be translated as a string
try self.assertSchemasTranslation(
"""
schemas:
MyUUID:
type: string
format: uuid
""",
"""
public enum Schemas {
public typealias MyUUID = Swift.String
}
"""
)
}

func testComponentsSchemasBase64() throws {
try self.assertSchemasTranslation(
Expand Down

0 comments on commit d7dcae0

Please sign in to comment.