Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
304 lines (259 sloc) 13.1 KB
//
// swift-code-generation.swift
// json2swift
//
// Created by Joshua Smith on 10/14/16.
// Copyright © 2016 iJoshSmith. All rights reserved.
//
import Foundation
struct SwiftCodeGenerator {
/// This method is used when only one Swift file is being generated.
static func generateCodeWithJSONUtilities(for swiftStruct: SwiftStruct) -> String {
return [
preamble,
"//",
"// MARK: - Data Model",
"//",
swiftStruct.toSwiftCode(),
"",
"//",
"// MARK: - JSON Utilities",
"//",
jsonUtilitiesTemplate,
""].joined(separator: "\n")
}
/// This method is used when multiple Swift files are being generated.
static func generateCode(for swiftStruct: SwiftStruct) -> String {
return [
preamble,
swiftStruct.toSwiftCode(),
""].joined(separator: "\n")
}
/// This method is used to only create the JSON utility code once when multiple Swift files are being generated.
static func generateJSONUtilities() -> String {
return [
preamble,
jsonUtilitiesTemplate,
""].joined(separator: "\n")
}
private static let preamble = [
"// This file was generated by json2swift. https://github.com/ijoshsmith/json2swift",
"",
"import Foundation",
""].joined(separator: "\n")
}
// MARK: - Implementation
typealias SwiftCode = String
typealias LineOfCode = SwiftCode
fileprivate struct Indentation {
private let chars: String
private let level: Int
private let value: String
init(chars: String, level: Int = 0) {
precondition(level >= 0)
self.chars = chars
self.level = level
self.value = String(repeating: chars, count: level)
}
func apply(toLineOfCode lineOfCode: LineOfCode) -> LineOfCode {
return value + lineOfCode
}
func apply(toFirstLine firstLine: LineOfCode,
nestedLines generateNestedLines: (Indentation) -> [LineOfCode],
andLastLine lastLine: LineOfCode) -> [LineOfCode] {
let first = apply(toLineOfCode: firstLine)
let middle = generateNestedLines(self.increased())
let last = apply(toLineOfCode: lastLine)
return [first] + middle + [last]
}
private func increased() -> Indentation {
return Indentation(chars: chars, level: level + 1)
}
}
fileprivate extension SwiftStruct {
func toSwiftCode(indentedBy indentChars: String = " ") -> SwiftCode {
let indentation = Indentation(chars: indentChars)
let linesOfCode = toLinesOfCode(at: indentation)
return linesOfCode.joined(separator: "\n")
}
private func toLinesOfCode(at indentation: Indentation) -> [LineOfCode] {
return indentation.apply(
toFirstLine: "struct \(name): CreatableFromJSON { // TODO: Rename this struct",
nestedLines: linesOfCodeForMembers(at:),
andLastLine: "}")
}
private func linesOfCodeForMembers(at indentation: Indentation) -> [LineOfCode] {
return linesOfCodeForProperties(at: indentation)
+ initializer.toLinesOfCode(at: indentation)
+ failableInitializer.toLinesOfCode(at: indentation)
+ linesOfCodeForNestedStructs(at: indentation)
}
private func linesOfCodeForProperties(at indentation: Indentation) -> [LineOfCode] {
return sortedProperties.map { property in
let propertyCode = property.toLineOfCode()
return indentation.apply(toLineOfCode: propertyCode)
}
}
private var sortedProperties: [SwiftProperty] {
return properties.sorted { (lhs, rhs) -> Bool in
return lhs.name.compare(rhs.name) == .orderedAscending
}
}
private func linesOfCodeForNestedStructs(at indentation: Indentation) -> [LineOfCode] {
return sortedNestedStructs.flatMap { $0.toLinesOfCode(at: indentation) }
}
private var sortedNestedStructs: [SwiftStruct] {
return nestedStructs.sorted(by: { (lhs, rhs) -> Bool in
return lhs.name.compare(rhs.name) == .orderedAscending
})
}
}
fileprivate extension SwiftType {
func toSwiftCode() -> SwiftCode {
return isOptional ? name + "?" : name
}
}
fileprivate extension SwiftProperty {
func toLineOfCode() -> LineOfCode {
return "let \(name): \(type.toSwiftCode())"
}
}
fileprivate extension SwiftParameter {
func toSwiftCode() -> SwiftCode {
return "\(name): \(type.toSwiftCode())"
}
}
fileprivate extension SwiftInitializer {
func toLinesOfCode(at indentation: Indentation) -> [LineOfCode] {
return indentation.apply(
toFirstLine: "init(\(parameterList)) {",
nestedLines: linesOfCodeForPropertyAssignments(at:),
andLastLine: "}")
}
private var parameterList: SwiftCode {
return sortedParameters
.map { $0.toSwiftCode() }
.joined(separator: ", ")
}
private func linesOfCodeForPropertyAssignments(at indentation: Indentation) -> [LineOfCode] {
return sortedParameters
.map { "self.\($0.name) = \($0.name)" }
.map(indentation.apply(toLineOfCode:))
}
private var sortedParameters: [SwiftParameter] {
return parameters.sorted { (lhs, rhs) -> Bool in
return lhs.name.compare(rhs.name) == .orderedAscending
}
}
}
fileprivate extension SwiftFailableInitializer {
func toLinesOfCode(at indentation: Indentation) -> [LineOfCode] {
return indentation.apply(
toFirstLine: "init?(json: [String: Any]) {",
nestedLines: linesOfCodeInMethodBody(at:),
andLastLine: "}")
}
private func linesOfCodeInMethodBody(at indentation: Indentation) -> [LineOfCode] {
let linesOfCode = linesOfCodeForTransformations + [lineOfCodeForCallingInitializer]
return linesOfCode.map(indentation.apply(toLineOfCode:))
}
private var linesOfCodeForTransformations: [LineOfCode] {
let requiredTransformationLines = sortedRequiredTransformations.map { $0.guardedLetStatement }
let optionalTransformationLines = sortedOptionalTransformations.map { $0.letStatement }
return (requiredTransformationLines + optionalTransformationLines)
}
private var lineOfCodeForCallingInitializer: LineOfCode {
let sortedPropertyNames = (requiredTransformations + optionalTransformations).map { $0.propertyName }.sorted()
let labeledArguments = sortedPropertyNames.map { $0 + ": " + $0 }
let argumentList = labeledArguments.joined(separator: ", ")
return "self.init(" + argumentList + ")"
}
private var sortedRequiredTransformations: [TransformationFromJSON] {
return sort(transformations: requiredTransformations)
}
private var sortedOptionalTransformations: [TransformationFromJSON] {
return sort(transformations: optionalTransformations)
}
private func sort(transformations: [TransformationFromJSON]) -> [TransformationFromJSON] {
return transformations.sorted { (lhs, rhs) -> Bool in
return lhs.propertyName.compare(rhs.propertyName) == .orderedAscending
}
}
}
// Internal for unit test access.
internal extension TransformationFromJSON {
var propertyName: String {
switch self {
case let .toCustomStruct(_, propertyName, _): return propertyName
case let .toPrimitiveValue(_, propertyName, _): return propertyName
case let .toCustomStructArray(_, propertyName, _, _): return propertyName
case let .toPrimitiveValueArray(_, propertyName, _, _): return propertyName
}
}
var guardedLetStatement: LineOfCode {
return "guard \(letStatement) else { return nil }"
}
var letStatement: LineOfCode {
switch self {
case let .toCustomStruct( attributeName, propertyName, type): return TransformationFromJSON.letStatementForCustomStruct( attributeName, propertyName, type)
case let .toPrimitiveValue( attributeName, propertyName, type): return TransformationFromJSON.letStatementForPrimitiveValue(attributeName, propertyName, type)
case let .toCustomStructArray( attributeName, propertyName, elementType, hasOptionalElements): return TransformationFromJSON.letStatementForCustomStructArray( attributeName, propertyName, elementType, hasOptionalElements)
case let .toPrimitiveValueArray(attributeName, propertyName, elementType, hasOptionalElements): return TransformationFromJSON.letStatementForPrimitiveValueArray(attributeName, propertyName, elementType, hasOptionalElements)
}
}
private static func letStatementForCustomStruct(_ attributeName: String, _ propertyName: String, _ type: SwiftStruct) -> LineOfCode {
return "let \(propertyName) = \(type.name)(json: json, key: \"\(attributeName)\")"
}
private static func letStatementForPrimitiveValue(_ attributeName: String, _ propertyName: String, _ type: SwiftPrimitiveValueType) -> LineOfCode {
switch type {
case .any: return "let \(propertyName) = json[\"\(attributeName)\"] as? Any"
case .emptyArray: return "let \(propertyName) = json[\"\(attributeName)\"] as? [Any?]"
case .bool, .int, .string: return "let \(propertyName) = json[\"\(attributeName)\"] as? \(type.name)"
case .double: return "let \(propertyName) = Double(json: json, key: \"\(attributeName)\")" // Allows an integer to be interpreted as a double.
case .url: return "let \(propertyName) = URL(json: json, key: \"\(attributeName)\")"
case .date(let format): return "let \(propertyName) = Date(json: json, key: \"\(attributeName)\", format: \"\(format)\")"
}
}
private static func letStatementForCustomStructArray(_ attributeName: String, _ propertyName: String, _ elementType: SwiftStruct, _ hasOptionalElements: Bool) -> LineOfCode {
return hasOptionalElements
? "let \(propertyName) = \(elementType.name).createOptionalInstances(from: json, arrayKey: \"\(attributeName)\")"
: "let \(propertyName) = \(elementType.name).createRequiredInstances(from: json, arrayKey: \"\(attributeName)\")"
}
private static func letStatementForPrimitiveValueArray(_ attributeName: String, _ propertyName: String, _ elementType: SwiftPrimitiveValueType, _ hasOptionalElements: Bool) -> LineOfCode {
return hasOptionalElements
? letStatementForArrayOfOptionalPrimitiveValues(attributeName, propertyName, elementType)
: letStatementForArrayOfRequiredPrimitiveValues(attributeName, propertyName, elementType)
}
private static func letStatementForArrayOfOptionalPrimitiveValues(_ attributeName: String, _ propertyName: String, _ elementType: SwiftPrimitiveValueType) -> LineOfCode {
switch elementType {
case .any, .bool, .int, .string, .emptyArray: return "let \(propertyName) = (json[\"\(attributeName)\"] as? [Any]).map({ $0.toOptionalValueArray() as [\(elementType.name)?] })"
case .date(let format): return "let \(propertyName) = (json[\"\(attributeName)\"] as? [Any]).map({ $0.toOptionalDateArray(withFormat: \"\(format)\") })"
case .double: return "let \(propertyName) = (json[\"\(attributeName)\"] as? [Any]).map({ $0.toOptionalDoubleArray() })"
case .url: return "let \(propertyName) = (json[\"\(attributeName)\"] as? [Any]).map({ $0.toOptionalURLArray() })"
}
}
private static func letStatementForArrayOfRequiredPrimitiveValues(_ attributeName: String, _ propertyName: String, _ elementType: SwiftPrimitiveValueType) -> LineOfCode {
switch elementType {
case .any: return "let \(propertyName) = json[\"\(attributeName)\"] as? [Any?]" // Any is treated as optional.
case .emptyArray: return "let \(propertyName) = json[\"\(attributeName)\"] as? [[Any?]]"
case .bool, .int, .string: return "let \(propertyName) = json[\"\(attributeName)\"] as? [\(elementType.name)]"
case .date(let format): return "let \(propertyName) = (json[\"\(attributeName)\"] as? [String]).flatMap({ $0.toDateArray(withFormat: \"\(format)\") })"
case .double: return "let \(propertyName) = (json[\"\(attributeName)\"] as? [NSNumber]).map({ $0.toDoubleArray() })"
case .url: return "let \(propertyName) = (json[\"\(attributeName)\"] as? [String]).flatMap({ $0.toURLArray() })"
}
}
}
fileprivate extension SwiftPrimitiveValueType {
var name: String {
switch self {
case .any: return "Any"
case .bool: return "Bool"
case .date: return "Date"
case .double: return "Double"
case .emptyArray: return "[Any?]"
case .int: return "Int"
case .string: return "String"
case .url: return "URL"
}
}
}