Created
May 7, 2022 17:40
-
-
Save adurbalo/eddf3c1586303323d5f9baafdccffa4f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<% | |
func capitalizedName(for variable: Variable) -> String { | |
return "\(String(variable.name.first!).capitalized)\(String(variable.name.dropFirst()))" | |
} | |
func customDecodingMethod(for variable: Variable, of type: Type) -> SourceryMethod? { | |
return type.staticMethods.first { $0.selectorName == "decode\(capitalizedName(for: variable))(from:)" } | |
} | |
func defaultDecodingValue(for variable: Variable, of type: Type) -> Variable? { | |
return type.staticVariables.first { $0.name == "default\(capitalizedName(for: variable))" } | |
} | |
func decodingContainerMethod(for type: Type) -> SourceryMethod? { | |
if let enumType = type as? Enum, !enumType.hasAssociatedValues { | |
return SourceryMethod(name: "singleValueContainer", throws: true) | |
} | |
return type.staticMethods.first { $0.selectorName == "decodingContainer(_:)" } | |
} | |
func customEncodingMethod(for variable: Variable, of type: Type) -> SourceryMethod? { | |
return type.instanceMethods.first { $0.selectorName == "encode\(capitalizedName(for: variable))(to:)" } | |
} | |
func encodeAdditionalVariablesMethod(for type: Type) -> SourceryMethod? { | |
return type.instanceMethods.first { $0.selectorName == "encodeAdditionalValues(to:)" } | |
} | |
func encodingContainerMethod(for type: Type) -> SourceryMethod? { | |
if let enumType = type as? Enum, !enumType.hasAssociatedValues { | |
return SourceryMethod(name: "singleValueContainer") | |
} | |
return type.instanceMethods.first { $0.selectorName == "encodingContainer(_:)" } | |
} | |
func typeHasMoreCodingKeysThanStoredProperties(_ type: Type, codingKeys: [String]) -> Bool { | |
let allKeysSet = Set(codingKeys) | |
let allStoredPropertiesNames = Set(type.storedVariables.map({ $0.name })) | |
let hasMoreKeys = allKeysSet.subtracting(allStoredPropertiesNames).count > 0 | |
return hasMoreKeys | |
} | |
func needsDecodableImplementation(for type: Type, codingKeys: (generated: [String], all: [String])) -> Bool { | |
guard type.implements["AutoDecodable"] != nil else { return false } | |
if let enumType = type as? Enum, enumType.rawTypeName == nil { return true } | |
if type.storedVariables.contains(where: { customDecodingMethod(for: $0, of: type) != nil }) { return true } | |
if type.storedVariables.contains(where: { defaultDecodingValue(for: $0, of: type) != nil }) { return true } | |
if decodingContainerMethod(for: type) != nil { return true } | |
if typeHasMoreCodingKeysThanStoredProperties(type, codingKeys: codingKeys.all) { return true } | |
return false | |
} | |
func needsEncodableImplementation(for type: Type, codingKeys: (generated: [String], all: [String])) -> Bool { | |
guard type.implements["AutoEncodable"] != nil else { return false } | |
if let enumType = type as? Enum, enumType.rawTypeName == nil { return true } | |
if type.variables.contains(where: { customEncodingMethod(for: $0, of: type) != nil }) { return true } | |
if encodeAdditionalVariablesMethod(for: type) != nil { return true } | |
if decodingContainerMethod(for: type) != nil { return true } | |
if typeHasMoreCodingKeysThanStoredProperties(type, codingKeys: codingKeys.all) { return true } | |
if ((type.containedType["SkipEncodingKeys"] as? Enum)?.cases.count ?? 0) > 0 { return true } | |
return false | |
} | |
func codingKeysFor(_ type: Type) -> (generated: [String], all: [String]) { | |
var generatedKeys = [String]() | |
var allCodingKeys = [String]() | |
if type is Struct { | |
if let codingKeysType = type.containedType["CodingKeys"] as? Enum { | |
allCodingKeys = codingKeysType.cases.map({ $0.name }) | |
let definedKeys = Set(allCodingKeys) | |
let storedVariablesKeys = type.storedVariables.filter({ $0.defaultValue == nil }).map({ $0.name }) | |
let computedVariablesKeys = type.computedVariables.filter({ customEncodingMethod(for: $0, of: type) != nil }).map({ $0.name }) | |
if (storedVariablesKeys.count + computedVariablesKeys.count) > definedKeys.count { | |
for key in storedVariablesKeys where !definedKeys.contains(key) { | |
generatedKeys.append(key) | |
allCodingKeys.append(key) | |
} | |
for key in computedVariablesKeys where !definedKeys.contains(key) { | |
generatedKeys.append(key) | |
allCodingKeys.append(key) | |
} | |
} | |
} else { | |
for variable in type.storedVariables { | |
generatedKeys.append(variable.name) | |
allCodingKeys.append(variable.name) | |
} | |
for variable in type.computedVariables { | |
guard customEncodingMethod(for: variable, of: type) != nil else { continue } | |
generatedKeys.append(variable.name) | |
allCodingKeys.append(variable.name) | |
} | |
} | |
} else if let enumType = type as? Enum { | |
var casesKeys: [String] = enumType.cases.map({ $0.name }) | |
if enumType.hasAssociatedValues { | |
enumType.cases | |
.flatMap({ $0.associatedValues }) | |
.compactMap({ $0.localName }) | |
.forEach({ | |
if !casesKeys.contains($0) { | |
casesKeys.append($0) | |
} | |
}) | |
} | |
if let codingKeysType = type.containedType["CodingKeys"] as? Enum { | |
allCodingKeys = codingKeysType.cases.map({ $0.name }) | |
let definedKeys = Set(allCodingKeys) | |
if casesKeys.count > definedKeys.count { | |
for key in casesKeys where !definedKeys.contains(key) { | |
generatedKeys.append(key) | |
allCodingKeys.append(key) | |
} | |
} | |
} else { | |
allCodingKeys = casesKeys | |
generatedKeys = allCodingKeys | |
} | |
} | |
return (generated: generatedKeys, all: allCodingKeys) | |
} | |
-%> | |
<%_ for type in types.all | |
where (type is Struct || type is Enum) | |
&& (type.implements["AutoDecodable"] != nil || type.implements["AutoEncodable"] != nil) { -%> | |
<%_ let codingKeys = codingKeysFor(type) -%> | |
<%_ if let codingKeysType = type.containedType["CodingKeys"] as? Enum, codingKeys.generated.count > 0 { -%> | |
// sourcery:inline:auto:<%= codingKeysType.name %>.AutoCodable | |
<%_ for key in codingKeys.generated { -%> | |
case <%= key %> | |
<%_ } -%> | |
// sourcery:end | |
<%_ } -%> | |
<%_ let typeNeedsDecodableImplementation = needsDecodableImplementation(for: type, codingKeys: codingKeys) -%> | |
<%_ let typeNeedsEncodableImplementation = needsEncodableImplementation(for: type, codingKeys: codingKeys) -%> | |
<%_ guard typeNeedsDecodableImplementation || typeNeedsEncodableImplementation else { continue } -%> | |
extension <%= type.name %> { | |
<%_ if type.containedType["CodingKeys"] as? Enum == nil { -%> | |
enum CodingKeys: String, CodingKey { | |
<%_ for key in codingKeys.generated { -%> | |
case <%= key %> | |
<%_ } -%> | |
} | |
<%_ }-%> | |
<%_ if typeNeedsDecodableImplementation { -%> | |
<%= type.accessLevel %> init(from decoder: Decoder) throws { | |
<%_ if let containerMethod = decodingContainerMethod(for: type) { -%> | |
let container = <% if containerMethod.throws { %>try <% } -%> | |
<%_ if containerMethod.callName == "singleValueContainer" { %>decoder<% } else { -%><%= type.name %><% } -%> | |
<%_ %>.<%= containerMethod.callName %>(<% if !containerMethod.parameters.isEmpty { %>decoder<% } %>) | |
<%_ } else { -%> | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
<%_ } -%> | |
<%_ if let enumType = type as? Enum { -%> | |
<%_ if enumType.hasAssociatedValues { -%> | |
<%_ if codingKeys.all.contains("enumCaseKey") { -%> | |
let enumCase = try container.decode(String.self, forKey: .enumCaseKey) | |
switch enumCase { | |
<%_ for enumCase in enumType.cases { -%> | |
case CodingKeys.<%= enumCase.name %>.rawValue: | |
<%_ if enumCase.associatedValues.isEmpty { -%> | |
self = .<%= enumCase.name %> | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> | |
// Enum cases with unnamed associated values can't be decoded | |
throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Can't decode '\(enumCase)'") | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> | |
<%_ for associatedValue in enumCase.associatedValues { -%> | |
let <%= associatedValue.localName! %> = try container.decode(<%= associatedValue.typeName %>.self, forKey: .<%= associatedValue.localName! %>) | |
<%_ } -%> | |
self = .<%= enumCase.name %>(<% -%> | |
<%_ %><%= enumCase.associatedValues.map({ "\($0.localName!): \($0.localName!)" }).joined(separator: ", ") %>) | |
<%_ } else { -%> | |
// Enum cases with mixed named and unnamed associated values can't be decoded | |
throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Can't decode '\(enumCase)'") | |
<%_ } -%> | |
<%_ } -%> | |
default: | |
throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Unknown enum case '\(enumCase)'") | |
} | |
<%_ } else { -%> | |
<%_ for enumCase in enumType.cases { -%> | |
if container.allKeys.contains(.<%= enumCase.name %>), try container.decodeNil(forKey: .<%= enumCase.name %>) == false { | |
<%_ if enumCase.associatedValues.isEmpty { -%> | |
self = .<%= enumCase.name %> | |
return | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> | |
var associatedValues = try container.nestedUnkeyedContainer(forKey: .<%= enumCase.name %>) | |
<%_ for (index, associatedValue) in enumCase.associatedValues.enumerated() { -%> | |
let associatedValue<%= index %> = try associatedValues.decode(<%= associatedValue.typeName %>.self) | |
<%_ } -%> | |
self = .<%= enumCase.name %>(<% -%> | |
<%_ %><%= enumCase.associatedValues.indices.map({ "associatedValue\($0)" }).joined(separator: ", ") %>) | |
return | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> | |
let associatedValues = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) | |
<%_ for associatedValue in enumCase.associatedValues { -%> | |
let <%= associatedValue.localName! %> = try associatedValues.decode(<%= associatedValue.typeName %>.self, forKey: .<%= associatedValue.localName! %>) | |
<%_ } -%> | |
self = .<%= enumCase.name %>(<% -%> | |
<%_ %><%= enumCase.associatedValues.map({ "\($0.localName!): \($0.localName!)" }).joined(separator: ", ") %>) | |
return | |
<%_ } else { -%> | |
// Enum cases with mixed named and unnamed associated values can't be decoded | |
throw DecodingError.dataCorruptedError(forKey: .<%= enumCase.name %>, in: container, debugDescription: "Can't decode `.<%= enumCase.name %>`") | |
<%_ } -%> | |
} | |
<%_ } -%> | |
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) | |
<%_ } -%> | |
<%_ } else { -%> | |
let enumCase = try container.decode(String.self) | |
switch enumCase { | |
<%_ for enumCase in enumType.cases { -%> | |
case CodingKeys.<%= enumCase.name %>.rawValue: self = .<%= enumCase.name %> | |
<%_ } -%> | |
default: throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case '\(enumCase)'")) | |
} | |
<%_ } -%> | |
<%_ } else { -%> | |
<%_ for key in codingKeys.all { -%> | |
<%_ guard let variable = type.instanceVariables.first(where: { $0.name == key && !$0.isComputed }) else { continue } -%> | |
<%_ let defaultValue = defaultDecodingValue(for: variable, of: type) -%> | |
<%_ let customMethod = customDecodingMethod(for: variable, of: type) -%> | |
<%_ let shouldTry = customMethod?.throws == true || customMethod == nil -%> | |
<%_ let shouldWrapTry = shouldTry && defaultValue != nil -%> | |
<%= variable.name %> = <% if shouldWrapTry { %>(try? <% } else if shouldTry { %>try <% } -%> | |
<%_ if let customMethod = customMethod { -%> | |
<%_ %><%= type.name %>.<%= customMethod.callName %>(from: <% if customMethod.parameters.first?.name == "decoder" { %>decoder<% } else { %>container<% } %>)<% -%> | |
<%_ } else { -%> | |
<%_ %>container.decode<% if variable.isOptional { %>IfPresent<% } %>(<%= variable.unwrappedTypeName %>.self, forKey: .<%= variable.name %>)<% -%> | |
<%_ } -%> | |
<%_ %><% if shouldWrapTry { %>)<% } -%> | |
<%_ if let defaultValue = defaultValue { %> ?? <%= type.name %>.<%= defaultValue.name -%><%_ } %> | |
<%_ } -%> | |
<%_ } -%> | |
} | |
<%_ } -%> | |
<%_ if typeNeedsEncodableImplementation { -%> | |
<%= type.accessLevel %> func encode(to encoder: Encoder) throws { | |
<%_ if let containerMethod = encodingContainerMethod(for: type) { -%> | |
var container = <% if containerMethod.callName == "singleValueContainer" { %>encoder.<% } %><%= containerMethod.callName %>(<% if !containerMethod.parameters.isEmpty { %>encoder<% } %>) | |
<%_ } else { -%> | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
<%_ } -%> | |
<%_ if let enumType = type as? Enum { -%> | |
<%_ if enumType.hasAssociatedValues { -%> | |
<%_ if codingKeys.all.contains("enumCaseKey") { -%> | |
switch self { | |
<%_ for enumCase in enumType.cases { -%> | |
<%_ if enumCase.associatedValues.isEmpty { -%> | |
case .<%= enumCase.name %>: | |
try container.encode(CodingKeys.<%= enumCase.name %>.rawValue, forKey: .enumCaseKey) | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> | |
case .<%= enumCase.name %>: | |
// Enum cases with unnamed associated values can't be encoded | |
throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> | |
case let .<%= enumCase.name %>(<%= enumCase.associatedValues.map({ "\($0.localName!)" }).joined(separator: ", ") %>): | |
try container.encode(CodingKeys.<%= enumCase.name %>.rawValue, forKey: .enumCaseKey) | |
<%_ for accociatedValue in enumCase.associatedValues { -%> | |
try container.encode(<%= accociatedValue.localName! %>, forKey: .<%= accociatedValue.localName! %>) | |
<%_ } -%> | |
<%_ } else { -%> | |
case .<%= enumCase.name %>: | |
// Enum cases with mixed named and unnamed associated values can't be encoded | |
throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) | |
<%_ } -%> | |
<%_ } -%> | |
} | |
<%_ } else { -%> | |
switch self { | |
<%_ for enumCase in enumType.cases { -%> | |
<%_ if enumCase.associatedValues.isEmpty { -%> | |
case .<%= enumCase.name %>: | |
_ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> | |
case let .<%= enumCase.name %>(<%= enumCase.associatedValues.indices.map({ "associatedValue\($0)" }).joined(separator: ", ") %>): | |
var associatedValues = container.nestedUnkeyedContainer(forKey: .<%= enumCase.name %>) | |
<%_ for (index, associatedValue) in enumCase.associatedValues.enumerated() { -%> | |
try associatedValues.encode(associatedValue<%= index %>) | |
<%_ } -%> | |
<%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> | |
case let .<%= enumCase.name %>(<%= enumCase.associatedValues.map({ "\($0.localName!)" }).joined(separator: ", ") %>): | |
var associatedValues = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) | |
<%_ for accociatedValue in enumCase.associatedValues { -%> | |
try associatedValues.encode(<%= accociatedValue.localName! %>, forKey: .<%= accociatedValue.localName! %>) | |
<%_ } -%> | |
<%_ } else { -%> | |
case .<%= enumCase.name %>: | |
// Enum cases with mixed named and unnamed associated values can't be encoded | |
throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) | |
<%_ } -%> | |
<%_ } -%> | |
} | |
<%_ } -%> | |
<%_ } else { -%> | |
switch self { | |
<%_ for enumCase in enumType.cases { -%> | |
case .<%= enumCase.name %>: try container.encode(CodingKeys.<%= enumCase.name %>.rawValue) | |
<%_ } -%> | |
} | |
<%_ } -%> | |
<%_ } else { -%> | |
<%_ let skipKeys = type.containedType["SkipEncodingKeys"] as? Enum -%> | |
<%_ for key in codingKeys.all { -%> | |
<%_ if let skipKeys = skipKeys, skipKeys.cases.contains(where: { $0.name == key }) { continue } -%> | |
<%_ guard let variable = type.instanceVariables.first(where: { $0.name == key }) ?? type.computedVariables.first(where: { $0.name == key }) else { continue } -%> | |
<%_ let customMethod = customEncodingMethod(for: variable, of: type) -%> | |
<%_ if let customMethod = customMethod { -%> | |
<% if customMethod.throws { %>try <% } %><%= customMethod.callName %>(to: <% if customMethod.parameters.first?.name == "encoder" { %>encoder<% } else { %>&container<% } %>) | |
<%_ } else { -%> | |
try container.encode<% if variable.isOptional { %>IfPresent<% } %>(<%= variable.name %>, forKey: .<%= variable.name %>) | |
<%_ } -%> | |
<%_ } -%> | |
<%_ if let encodeAdditional = encodeAdditionalVariablesMethod(for: type) { -%> | |
<% if encodeAdditional.throws { %>try <% } %><%= encodeAdditional.callName %>(to: <% if encodeAdditional.parameters.first?.name == "encoder" { %>encoder<% } else { %>&container<% } %>) | |
<%_ } -%> | |
<%_ } -%> | |
} | |
<%_ } -%> | |
} | |
<% } -%> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment