Skip to content

Commit 80896d9

Browse files
authored
Merge pull request #16 from mattpolzin/more-poly-support
More poly support
2 parents 8c38b51 + b6cf3d7 commit 80896d9

File tree

5 files changed

+179
-13
lines changed

5 files changed

+179
-13
lines changed

Sources/JSONAPISwiftGen/Swift Generators/ResourceObjectSwiftGen.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ public struct ResourceObjectSwiftGen: JSONSchemaSwiftGenerator, ResourceTypeSwif
271271
return (attributes: attributesDecl, dependencies: attributeDecls.flatMap { $0.1 })
272272
}
273273

274+
/// Takes a JSONSchema and attempts to create a single attribute's
275+
/// code snippet (Decl).
274276
private static func attributeSnippet(
275277
name: String,
276278
schema: DereferencedJSONSchema,
@@ -284,7 +286,7 @@ public struct ResourceObjectSwiftGen: JSONSchemaSwiftGenerator, ResourceTypeSwif
284286
let dependencies: [Decl]
285287

286288
switch schema {
287-
case .object:
289+
case .object, .one:
288290
let structureGen = try StructureSwiftGen(
289291
swiftTypeName: typeCased(name),
290292
structure: schema,

Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,37 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator {
3131
cascadingConformances: [String] = [],
3232
rootConformances: [String]? = nil
3333
) throws {
34-
guard case .object(_, let context) = structure else {
35-
throw Error.rootNotJSONObject
34+
let typeName: String
35+
if reservedTypeNames.contains(swiftTypeName) {
36+
typeName = "Gen" + swiftTypeName
37+
} else {
38+
typeName = swiftTypeName
3639
}
3740

38-
self.swiftTypeName = swiftTypeName
41+
self.swiftTypeName = typeName
3942
self.structure = structure
4043

41-
decls = [
42-
try StructureSwiftGen.structure(
43-
named: swiftTypeName,
44-
forObject: context,
44+
switch structure {
45+
case .object(_, let context):
46+
decls = [
47+
try StructureSwiftGen.structure(
48+
named: typeName,
49+
forObject: context,
50+
cascadingConformances: cascadingConformances,
51+
rootConformances: rootConformances
52+
)
53+
]
54+
case .one(of: let schemas, core: _):
55+
let poly = try StructureSwiftGen.structure(
56+
named: typeName,
57+
forOneOf: schemas,
4558
cascadingConformances: cascadingConformances,
4659
rootConformances: rootConformances
4760
)
48-
]
61+
decls = [poly.polyDecl] + poly.dependencies
62+
default:
63+
throw Error.rootNotJSONObject
64+
}
4965
}
5066

5167
static func structure(
@@ -72,6 +88,40 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator {
7288
)
7389
}
7490

91+
static func structure(
92+
named name: String,
93+
forOneOf schemas: [DereferencedJSONSchema],
94+
cascadingConformances: [String],
95+
rootConformances: [String]? = nil
96+
) throws -> (polyDecl: Decl, dependencies: [Decl]) {
97+
let dependencies = try schemas
98+
.enumerated()
99+
.map { (idx, schema) -> (String, [Decl]) in
100+
let name = typeCased("Poly\(name)\(idx)")
101+
return (
102+
name,
103+
try declsForType(
104+
named: name,
105+
for: schema,
106+
conformances: cascadingConformances
107+
)
108+
)
109+
}
110+
111+
let poly = Typealias(
112+
alias: .def(.init(name: name)),
113+
existingType: .def(
114+
.init(
115+
name: "Poly\(dependencies.count)",
116+
specializationReps: dependencies.map{ .def(.init(name: $0.0)) },
117+
optional: false
118+
)
119+
)
120+
)
121+
122+
return (polyDecl: poly, dependencies: dependencies.flatMap(\.1))
123+
}
124+
75125
static func structure(
76126
named name: String,
77127
forArray context: DereferencedJSONSchema.ArrayContext,
@@ -91,6 +141,70 @@ public struct StructureSwiftGen: JSONSchemaSwiftGenerator {
91141
)
92142
}
93143

144+
/// Create the decls needed to represent the structures in use
145+
/// by a PolyX.
146+
static func declsForType(
147+
named name: String,
148+
for schema: DereferencedJSONSchema,
149+
conformances: [String]
150+
) throws -> [Decl] {
151+
let type: SwiftTypeRep
152+
let structureDecl: Decl?
153+
154+
do {
155+
type = try swiftType(from: schema, allowPlaceholders: false)
156+
structureDecl = nil
157+
} catch {
158+
switch schema {
159+
case .object(let context, let objContext):
160+
let newTypeName = typeCased(name)
161+
162+
// TODO: ideally distinguish between these
163+
// but that requires generating Swift code
164+
// for custom encoding/decoding
165+
let optional = !context.required || context.nullable
166+
167+
let typeIntermediate = SwiftTypeRep.def(.init(name: newTypeName))
168+
169+
type = optional ? typeIntermediate.optional : typeIntermediate
170+
171+
structureDecl = try structure(
172+
named: newTypeName,
173+
forObject: objContext,
174+
cascadingConformances: conformances
175+
)
176+
177+
case .array(let context, let arrayContext):
178+
let newTypeName = typeCased(name)
179+
180+
// TODO: ideally distinguish between these
181+
// but that requires generating Swift code
182+
// for custom encoding/decoding
183+
let optional = !context.required || context.nullable
184+
185+
let typeIntermediate = SwiftTypeRep.def(SwiftTypeDef(name: newTypeName).array)
186+
187+
type = optional ? typeIntermediate.optional : typeIntermediate
188+
189+
structureDecl = try structure(
190+
named: newTypeName,
191+
forArray: arrayContext,
192+
conformances: conformances
193+
)
194+
default:
195+
throw SwiftTypeError.typeNotFound
196+
}
197+
}
198+
return [
199+
structureDecl ?? Typealias(
200+
alias: .def(.init(name: name)),
201+
existingType: type
202+
)
203+
].compactMap { $0 }
204+
}
205+
206+
/// Create the decls needed to represent the substructure of
207+
/// a property with the given name.
94208
static func declsForProp(
95209
named name: String,
96210
for schema: DereferencedJSONSchema,

Sources/JSONAPISwiftGen/SwiftDecls.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ public struct Import: Decl {
390390
public static let JSONAPI: Import = .init(module: "JSONAPI")
391391
public static let JSONAPITesting: Import = .init(module: "JSONAPITesting")
392392
public static let OpenAPIKit: Import = .init(module: "OpenAPIKit")
393+
public static let Poly: Import = .init(module: "Poly")
393394

394395
public static let FoundationNetworking: Decl = """
395396
#if canImport(FoundationNetworking)

Sources/JSONAPISwiftGen/SwiftGen.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import Foundation
99
import OpenAPIKit
1010
import JSONAPI
1111

12+
/// A relatively ad-hoc list of names that if used for generated types in the
13+
/// wrong context could result in code ambiguity.
14+
internal let reservedTypeNames = [
15+
"Metadata",
16+
"Attributes",
17+
"Relationships"
18+
]
19+
1220
public protocol SwiftGenerator: SwiftCodeRepresentable {
1321
var decls: [Decl] { get }
1422
}

Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import OpenAPIKit
77
import JSONAPIOpenAPI
88

99
let testEncoder = JSONEncoder()
10+
let testDecoder = JSONDecoder()
1011

1112
class ResourceObjectSwiftGenTests: XCTestCase {
1213
func test_DirectConstruction() {
@@ -59,14 +60,54 @@ class ResourceObjectSwiftGenTests: XCTestCase {
5960
print(try! person.formattedSwiftCode())
6061
}
6162

62-
func test_ViaOpenAPI() {
63-
let openAPIStructure = try! TestPerson.openAPISchema(using: testEncoder).dereferenced()!
63+
func test_ViaOpenAPI() throws {
64+
let openAPIStructure = try TestPerson.openAPISchema(using: testEncoder).dereferenced()!
6465

65-
let testPersonSwiftGen = try! ResourceObjectSwiftGen(structure: openAPIStructure)
66+
let testPersonSwiftGen = try ResourceObjectSwiftGen(structure: openAPIStructure)
6667

6768
XCTAssertEqual(testPersonSwiftGen.resourceTypeName, "TestPerson")
6869

69-
print(try! testPersonSwiftGen.formattedSwiftCode())
70+
print(try testPersonSwiftGen.formattedSwiftCode())
71+
}
72+
73+
func test_polyAttribute() throws {
74+
let openAPIStructure = try testDecoder.decode(
75+
JSONSchema.self,
76+
from: """
77+
{
78+
"type": "object",
79+
"properties": {
80+
"type": {"type": "string", "enum": ["poly_thing"]},
81+
"id": {"type": "string"},
82+
"attributes": {
83+
"type": "object",
84+
"properties": {
85+
"poly_property": {
86+
"oneOf" : [
87+
{"type": "string"},
88+
{"type": "number"},
89+
{"type": "array", "items": {"type": "string"}},
90+
{
91+
"type": "object",
92+
"properties": {
93+
"foo": {"type": "string", "format": "date"},
94+
"bar": {"type": "object"}
95+
}
96+
}
97+
]
98+
}
99+
}
100+
}
101+
}
102+
}
103+
""".data(using: .utf8)!
104+
).dereferenced()!
105+
106+
let polyAttrSwiftGen = try ResourceObjectSwiftGen(structure: openAPIStructure)
107+
108+
XCTAssertEqual(polyAttrSwiftGen.resourceTypeName, "PolyThing")
109+
110+
print(polyAttrSwiftGen.swiftCode)
70111
}
71112
}
72113

0 commit comments

Comments
 (0)