From 96e82e6009ccefc771d98b4217f1ea1092bbdcd3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 17:13:21 -0800 Subject: [PATCH 1/6] add support for oneOf in generic swift structure generation via Poly. --- .../ResourceObjectSwiftGen.swift | 4 +- .../Swift Generators/StructureSwiftGen.swift | 123 ++++++++++++++++-- .../ResourceObjectSwiftGenTests.swift | 49 ++++++- 3 files changed, 163 insertions(+), 13 deletions(-) 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..4bde1d7 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -31,21 +31,30 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { cascadingConformances: [String] = [], rootConformances: [String]? = nil ) throws { - guard case .object(_, let context) = structure else { - throw Error.rootNotJSONObject - } - self.swiftTypeName = swiftTypeName self.structure = structure - decls = [ - try StructureSwiftGen.structure( + switch structure { + case .object(_, let context): + decls = [ + try StructureSwiftGen.structure( + named: swiftTypeName, + forObject: context, + cascadingConformances: cascadingConformances, + rootConformances: rootConformances + ) + ] + case .one(of: let schemas, core: _): + let poly = try StructureSwiftGen.structure( named: swiftTypeName, - forObject: context, + forOneOf: schemas, cascadingConformances: cascadingConformances, rootConformances: rootConformances ) - ] + decls = [poly.polyDecl] + poly.dependencies + default: + throw Error.rootNotJSONObject + } } static func structure( @@ -72,6 +81,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("\(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 +134,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/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) } } From 954b978e54c926fbbd935e12139b219d870ee89e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 17:16:57 -0800 Subject: [PATCH 2/6] Add Poly import decl. --- Sources/JSONAPISwiftGen/SwiftDecls.swift | 1 + 1 file changed, 1 insertion(+) 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) From c41cce0e1fca8a225813b7cc82a694433ddca5a1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 20:27:29 -0800 Subject: [PATCH 3/6] hedge bets against colliding with JSONAPI types when names are a bit close (like 'metadata') --- .../JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift index 4bde1d7..3cb0d42 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -90,7 +90,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { let dependencies = try schemas .enumerated() .map { (idx, schema) -> (String, [Decl]) in - let name = typeCased("\(name)\(idx)") + let name = typeCased("Poly\(name)\(idx)") return ( name, try declsForType( From 52542c38467b2fd5cc299eda58d41b2cddde1efd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 21:08:37 -0800 Subject: [PATCH 4/6] there's where I need to add the name prefix --- .../JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift index 3cb0d42..2b79554 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -102,7 +102,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { } let poly = Typealias( - alias: .def(.init(name: name)), + alias: .def(.init(name: "Poly\(name)")), existingType: .def( .init( name: "Poly\(dependencies.count)", From ff89229930b4bbf546ca3ed6d55e5ed309fe32dd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 22:22:25 -0800 Subject: [PATCH 5/6] try a different approach to renaming Metadata --- .../Swift Generators/StructureSwiftGen.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift index 2b79554..fc1b229 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -8,6 +8,10 @@ import Foundation import OpenAPIKit +fileprivate let collidingNames = [ + "Metadata" +] + /// Given some JSON Schema, attempt to generate Swift code for /// a `struct` that is capable of parsing data adhering to the schema. public struct StructureSwiftGen: JSONSchemaSwiftGenerator { @@ -31,7 +35,14 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { cascadingConformances: [String] = [], rootConformances: [String]? = nil ) throws { - self.swiftTypeName = swiftTypeName + let typeName: String + if collidingNames.contains(swiftTypeName) { + typeName = "Gen" + swiftTypeName + } else { + typeName = swiftTypeName + } + + self.swiftTypeName = typeName self.structure = structure switch structure { @@ -102,7 +113,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { } let poly = Typealias( - alias: .def(.init(name: "Poly\(name)")), + alias: .def(.init(name: name)), existingType: .def( .init( name: "Poly\(dependencies.count)", From b6cf3d70cb91f08831f0bb48ee8824266577cb21 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 15 Feb 2021 23:27:59 -0800 Subject: [PATCH 6/6] use the generated name in all the places. --- .../Swift Generators/StructureSwiftGen.swift | 10 +++------- Sources/JSONAPISwiftGen/SwiftGen.swift | 8 ++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift index fc1b229..961584a 100644 --- a/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift +++ b/Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift @@ -8,10 +8,6 @@ import Foundation import OpenAPIKit -fileprivate let collidingNames = [ - "Metadata" -] - /// Given some JSON Schema, attempt to generate Swift code for /// a `struct` that is capable of parsing data adhering to the schema. public struct StructureSwiftGen: JSONSchemaSwiftGenerator { @@ -36,7 +32,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { rootConformances: [String]? = nil ) throws { let typeName: String - if collidingNames.contains(swiftTypeName) { + if reservedTypeNames.contains(swiftTypeName) { typeName = "Gen" + swiftTypeName } else { typeName = swiftTypeName @@ -49,7 +45,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { case .object(_, let context): decls = [ try StructureSwiftGen.structure( - named: swiftTypeName, + named: typeName, forObject: context, cascadingConformances: cascadingConformances, rootConformances: rootConformances @@ -57,7 +53,7 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator { ] case .one(of: let schemas, core: _): let poly = try StructureSwiftGen.structure( - named: swiftTypeName, + named: typeName, forOneOf: schemas, cascadingConformances: cascadingConformances, rootConformances: rootConformances 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 } }