diff --git a/Sources/JSONAPISwiftGen/Swift Generators/ResourceObjectSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/ResourceObjectSwiftGen.swift index 2258f38..17189a8 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/ResourceObjectSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/ResourceObjectSwiftGen.swift @@ -271,6 +271,8 @@ public struct ResourceObjectSwiftGen: JSONSchemaSwiftGenerator, ResourceTypeSwif return (attributes: attributesDecl, dependencies: attributeDecls.flatMap { $0.1 }) } + /// Takes a JSONSchema and attempts to create a single attribute's + /// code snippet (Decl). private static func attributeSnippet( name: String, schema: DereferencedJSONSchema, @@ -284,7 +286,7 @@ public struct ResourceObjectSwiftGen: JSONSchemaSwiftGenerator, ResourceTypeSwif let dependencies: [Decl] switch schema { - case .object: + case .object, .one: let structureGen = try StructureSwiftGen( swiftTypeName: typeCased(name), structure: schema, diff --git a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift index 7a31925..961584a 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -31,21 +31,37 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { cascadingConformances: [String] = [], rootConformances: [String]? = nil ) throws { - guard case .object(_, let context) = structure else { - throw Error.rootNotJSONObject + let typeName: String + if reservedTypeNames.contains(swiftTypeName) { + typeName = "Gen" + swiftTypeName + } else { + typeName = swiftTypeName } - self.swiftTypeName = swiftTypeName + self.swiftTypeName = typeName self.structure = structure - decls = [ - try StructureSwiftGen.structure( - named: swiftTypeName, - forObject: context, + switch structure { + case .object(_, let context): + decls = [ + try StructureSwiftGen.structure( + named: typeName, + forObject: context, + cascadingConformances: cascadingConformances, + rootConformances: rootConformances + ) + ] + case .one(of: let schemas, core: _): + let poly = try StructureSwiftGen.structure( + named: typeName, + forOneOf: schemas, cascadingConformances: cascadingConformances, rootConformances: rootConformances ) - ] + decls = [poly.polyDecl] + poly.dependencies + default: + throw Error.rootNotJSONObject + } } static func structure( @@ -72,6 +88,40 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { ) } + static func structure( + named name: String, + forOneOf schemas: [DereferencedJSONSchema], + cascadingConformances: [String], + rootConformances: [String]? = nil + ) throws -> (polyDecl: Decl, dependencies: [Decl]) { + let dependencies = try schemas + .enumerated() + .map { (idx, schema) -> (String, [Decl]) in + let name = typeCased("Poly\(name)\(idx)") + return ( + name, + try declsForType( + named: name, + for: schema, + conformances: cascadingConformances + ) + ) + } + + let poly = Typealias( + alias: .def(.init(name: name)), + existingType: .def( + .init( + name: "Poly\(dependencies.count)", + specializationReps: dependencies.map{ .def(.init(name: $0.0)) }, + optional: false + ) + ) + ) + + return (polyDecl: poly, dependencies: dependencies.flatMap(\.1)) + } + static func structure( named name: String, forArray context: DereferencedJSONSchema.ArrayContext, @@ -91,6 +141,70 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { ) } + /// Create the decls needed to represent the structures in use + /// by a PolyX. + static func declsForType( + named name: String, + for schema: DereferencedJSONSchema, + conformances: [String] + ) throws -> [Decl] { + let type: SwiftTypeRep + let structureDecl: Decl? + + do { + type = try swiftType(from: schema, allowPlaceholders: false) + structureDecl = nil + } catch { + switch schema { + case .object(let context, let objContext): + let newTypeName = typeCased(name) + + // TODO: ideally distinguish between these + // but that requires generating Swift code + // for custom encoding/decoding + let optional = !context.required || context.nullable + + let typeIntermediate = SwiftTypeRep.def(.init(name: newTypeName)) + + type = optional ? typeIntermediate.optional : typeIntermediate + + structureDecl = try structure( + named: newTypeName, + forObject: objContext, + cascadingConformances: conformances + ) + + case .array(let context, let arrayContext): + let newTypeName = typeCased(name) + + // TODO: ideally distinguish between these + // but that requires generating Swift code + // for custom encoding/decoding + let optional = !context.required || context.nullable + + let typeIntermediate = SwiftTypeRep.def(SwiftTypeDef(name: newTypeName).array) + + type = optional ? typeIntermediate.optional : typeIntermediate + + structureDecl = try structure( + named: newTypeName, + forArray: arrayContext, + conformances: conformances + ) + default: + throw SwiftTypeError.typeNotFound + } + } + return [ + structureDecl ?? Typealias( + alias: .def(.init(name: name)), + existingType: type + ) + ].compactMap { $0 } + } + + /// Create the decls needed to represent the substructure of + /// a property with the given name. static func declsForProp( named name: String, for schema: DereferencedJSONSchema, diff --git a/Sources/JSONAPISwiftGen/SwiftDecls.swift b/Sources/JSONAPISwiftGen/SwiftDecls.swift index c712896..5185433 100644 --- a/Sources/JSONAPISwiftGen/SwiftDecls.swift +++ b/Sources/JSONAPISwiftGen/SwiftDecls.swift @@ -390,6 +390,7 @@ public struct Import: Decl { public static let JSONAPI: Import = .init(module: "JSONAPI") public static let JSONAPITesting: Import = .init(module: "JSONAPITesting") public static let OpenAPIKit: Import = .init(module: "OpenAPIKit") + public static let Poly: Import = .init(module: "Poly") public static let FoundationNetworking: Decl = """ #if canImport(FoundationNetworking) diff --git a/Sources/JSONAPISwiftGen/SwiftGen.swift b/Sources/JSONAPISwiftGen/SwiftGen.swift index a395b27..045e35d 100644 --- a/Sources/JSONAPISwiftGen/SwiftGen.swift +++ b/Sources/JSONAPISwiftGen/SwiftGen.swift @@ -9,6 +9,14 @@ import Foundation import OpenAPIKit import JSONAPI +/// A relatively ad-hoc list of names that if used for generated types in the +/// wrong context could result in code ambiguity. +internal let reservedTypeNames = [ + "Metadata", + "Attributes", + "Relationships" +] + public protocol SwiftGenerator: SwiftCodeRepresentable { var decls: [Decl] { get } } diff --git a/Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift b/Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift index e918336..1d6cbaf 100644 --- a/Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift +++ b/Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift @@ -7,6 +7,7 @@ import OpenAPIKit import JSONAPIOpenAPI let testEncoder = JSONEncoder() +let testDecoder = JSONDecoder() class ResourceObjectSwiftGenTests: XCTestCase { func test_DirectConstruction() { @@ -59,14 +60,54 @@ class ResourceObjectSwiftGenTests: XCTestCase { print(try! person.formattedSwiftCode()) } - func test_ViaOpenAPI() { - let openAPIStructure = try! TestPerson.openAPISchema(using: testEncoder).dereferenced()! + func test_ViaOpenAPI() throws { + let openAPIStructure = try TestPerson.openAPISchema(using: testEncoder).dereferenced()! - let testPersonSwiftGen = try! ResourceObjectSwiftGen(structure: openAPIStructure) + let testPersonSwiftGen = try ResourceObjectSwiftGen(structure: openAPIStructure) XCTAssertEqual(testPersonSwiftGen.resourceTypeName, "TestPerson") - print(try! testPersonSwiftGen.formattedSwiftCode()) + print(try testPersonSwiftGen.formattedSwiftCode()) + } + + func test_polyAttribute() throws { + let openAPIStructure = try testDecoder.decode( + JSONSchema.self, + from: """ + { + "type": "object", + "properties": { + "type": {"type": "string", "enum": ["poly_thing"]}, + "id": {"type": "string"}, + "attributes": { + "type": "object", + "properties": { + "poly_property": { + "oneOf" : [ + {"type": "string"}, + {"type": "number"}, + {"type": "array", "items": {"type": "string"}}, + { + "type": "object", + "properties": { + "foo": {"type": "string", "format": "date"}, + "bar": {"type": "object"} + } + } + ] + } + } + } + } + } + """.data(using: .utf8)! + ).dereferenced()! + + let polyAttrSwiftGen = try ResourceObjectSwiftGen(structure: openAPIStructure) + + XCTAssertEqual(polyAttrSwiftGen.resourceTypeName, "PolyThing") + + print(polyAttrSwiftGen.swiftCode) } }