Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
130 changes: 122 additions & 8 deletions Sources/JSONAPISwiftGen/Swift Generators/StructureSwiftGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions Sources/JSONAPISwiftGen/SwiftDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions Sources/JSONAPISwiftGen/SwiftGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
49 changes: 45 additions & 4 deletions Tests/JSONAPISwiftGenTests/ResourceObjectSwiftGenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import OpenAPIKit
import JSONAPIOpenAPI

let testEncoder = JSONEncoder()
let testDecoder = JSONDecoder()

class ResourceObjectSwiftGenTests: XCTestCase {
func test_DirectConstruction() {
Expand Down Expand Up @@ -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)
}
}

Expand Down