Skip to content

Instantly share code, notes, and snippets.

@adurbalo
Created May 7, 2022 17:40
Show Gist options
  • Save adurbalo/eddf3c1586303323d5f9baafdccffa4f to your computer and use it in GitHub Desktop.
Save adurbalo/eddf3c1586303323d5f9baafdccffa4f to your computer and use it in GitHub Desktop.
<%
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