Skip to content

Commit

Permalink
Adding missing tests and some minor refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
PJ Fechner authored and PJ Fechner committed Jan 8, 2025
1 parent e44fd94 commit 96a2d23
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 65 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let package = Package(
// .package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.52.0")),
.package(url: "https://github.com/swiftlang/swift-syntax.git", "508.0.0"..."600.0.1"),
// For testing different versions of swift-syntax
// .package(url: "https://github.com/apple/swift-syntax.git", .upToNextMajor(from: "509.0.0"))
// .package(url: "https://github.com/apple/swift-syntax.git", .upToNextMajor(from: "510.0.0"))

],
targets: [
Expand Down
49 changes: 31 additions & 18 deletions Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,35 +82,43 @@ enum CodingKeyCase {
/// custom casing
case custom((String) -> (String))

var separator: String {
var separator: String? {
switch self {
case .noChanges, .camelCase, .flatCase, .pascalCase, .upperCase:
""
case .snakeCase, .camelSnakeCase, .pascalSnakeCase, .screamingSnakeCase:
"_"
case .kebabCase, .camelKebabCase, .pascalKebabCase, .screamingKebabCase:
"-"
case .custom(_):
""
case .custom:
nil
}
}

var caseVariant: CaseVariant? {
switch self {
case .flatCase, .snakeCase, .kebabCase:
.lowerCase
case .camelCase, .camelSnakeCase, .camelKebabCase:
.camelCase
case .pascalCase, .pascalSnakeCase, .pascalKebabCase:
.pascalCase
case .upperCase, .screamingSnakeCase, .screamingKebabCase:
.upperCase
case .custom(_), .noChanges:
nil

Check warning on line 109 in Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift

View check run for this annotation

Codecov / codecov/patch

Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift#L109

Added line #L109 was not covered by tests
}
}

func makeKeyValue(from value: String) -> String {
switch self {
case .noChanges: value
case .camelCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .camelCase)
case .flatCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .lowerCase)
case .pascalCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .pascalCase)
case .upperCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .upperCase)
case .snakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .lowerCase)
case .camelSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .camelCase)
case .pascalSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .pascalCase)
case .screamingSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .upperCase)
case .kebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .lowerCase)
case .camelKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .camelCase)
case .pascalKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .pascalCase)
case .screamingKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .upperCase)
case .custom(let converter): converter(value)
case .noChanges: return value
case .custom(let converter): return converter(value)
default:
guard let keyConverter = KeyConverter(keyCase: self), let caseVariant else {
return value

Check warning on line 119 in Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift

View check run for this annotation

Codecov / codecov/patch

Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift#L119

Added line #L119 was not covered by tests
}
return keyConverter.convert(value: value, variant: caseVariant)
}
}
}
Expand All @@ -131,7 +139,12 @@ struct KeyConverter: Sendable {
init(separator: String) {
self.separator = separator
}


init?(keyCase: CodingKeyCase) {
guard let separator = keyCase.separator else { return nil }
self.init(separator: separator)
}

func convert(value: String, variant: CaseVariant) -> String {
// Remove any special characters at the beginning/end
let isAllCaps = value.isAllCaps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ extension MemberBlockItemListSyntax.Element {
decl.as(VariableDeclSyntax.self)?.attributes.matching(matching: T.self) ?? []
}

func attributeSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
decl.as(VariableDeclSyntax.self)?.attributes.matchingSyntax(matching: T.self) ?? []
}
// Not currently used
// func attributeSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
// decl.as(VariableDeclSyntax.self)?.attributes.matchingSyntax(matching: T.self) ?? []
// }

func attributeSyntax(named name: String) -> AttributeSyntax? {
attribute(named: name)?.as(AttributeSyntax.self)
Expand Down Expand Up @@ -57,14 +58,15 @@ extension AttributeListSyntax {
}
}

func matchingSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
compactMap {
guard let attributeName = $0.identifierName?.trimmingCharacters(in: .whitespacesAndNewlines), let syntax = $0.as(AttributeSyntax.self), let type = T(rawValue: attributeName) else {
return nil
}
return (type, syntax)
}
}
// Not currently used
// func matchingSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
// compactMap {
// guard let attributeName = $0.identifierName?.trimmingCharacters(in: .whitespacesAndNewlines), let syntax = $0.as(AttributeSyntax.self), let type = T(rawValue: attributeName) else {
// return nil
// }
// return (type, syntax)
// }
// }
}
extension AttributeListSyntax.Element {
var identifierName: String? {
Expand Down
7 changes: 2 additions & 5 deletions Sources/CodableWrapperMacros/ErrorHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@ extension AttributeSyntax {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .codingKeyValueRequired)])
}
let argument = argumentList[argumentList.index(argumentList.startIndex, offsetBy: index)]
guard argument.expression.is(StringLiteralExprSyntax.self) else {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .mustBeStringLiteral)])
}
// Uses the value in the Macro
// Get the value of the macro
guard let customKeyValue = argument.expression.as(StringLiteralExprSyntax.self) else {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .codingKeyValueRequired)])
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .mustBeStringLiteral)])
}

return ExprSyntax(customKeyValue)
Expand Down
26 changes: 14 additions & 12 deletions Sources/CodableWrapperMacros/TypeMacroContainers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ struct CodableMacroStructContainer {
}

func attributeNameCanGenerate(name: String?) -> Bool {
switch name {
case .none: false
case CustomCodable.macroName:
true
case CodingKeyPrefix.macroName:
!codableDefined
case CodingKeySuffix.macroName:
attributeNameCanGenerate(name: CodingKeyPrefix.macroName) && codingKeyPrefix == nil
default:
attributeNameCanGenerate(name: CodingKeySuffix.macroName)
&& name == codableAttributes.first?.attributeType.rawValue
}
name == CustomCodable.macroName
// Future work for other attributres being able to generate
// switch name {
// case .none: false
// case CustomCodable.macroName:
// true
// case CodingKeyPrefix.macroName:
// !codableDefined
// case CodingKeySuffix.macroName:
// attributeNameCanGenerate(name: CodingKeyPrefix.macroName) && codingKeyPrefix == nil
// default:
// attributeNameCanGenerate(name: CodingKeySuffix.macroName)
// && name == codableAttributes.first?.attributeType.rawValue
// }
}
}

Expand Down
1 change: 0 additions & 1 deletion Sources/CodableWrappers/StaticCoders/BoolCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public struct BoolAsStringValueProvider: NonConformingBoolValueProvider {
case "true": return true
case "false": return false
default:
print("Failed to convert \(typeValue) to Boolean return nil")
return nil
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodableWrappers/StaticCoders/DateCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ extension DateFormatterStaticDecoder {
let stringValue = try String(from: decoder)

guard let value = dateFormatter.date(from: stringValue) else {
let description = "Expected \(Data.self) but could not convert \(stringValue) to Data"
let description = "Expected \(Date.self) but could not convert \(stringValue) to Date"
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: description))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ public protocol NonConformingDecimalValueProvider {

/// Uses the `ValueProvider` for (de)serialization of a non-conforming `Float`
public struct NonConformingFloatStaticCoder<ValueProvider: NonConformingDecimalValueProvider>: StaticCoder {
private init() { }

public static func decode(from decoder: Decoder) throws -> Float {
guard let stringValue = try? String(from: decoder) else {
return try Float(from: decoder)
Expand Down Expand Up @@ -55,8 +53,6 @@ public struct NonConformingFloatStaticCoder<ValueProvider: NonConformingDecimalV

/// Uses the `ValueProvider` for (de)serialization of a non-conforming `Double`
public struct NonConformingDoubleStaticCoder<ValueProvider: NonConformingDecimalValueProvider>: StaticCoder {
private init() { }

public static func decode(from decoder: Decoder) throws -> Double {
guard let stringValue = try? String(from: decoder) else {
return try Double(from: decoder)
Expand Down
104 changes: 102 additions & 2 deletions Tests/CodableWrapperMacrosTests/CodingKeyMacroErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ final class CodingKeyMacroErrorTests: XCTestCase {
// macros: testMacros)
}

func testThrowsErrorWhenCodingKeyImplemented() throws {
func testThrowsErrorWhenCodingKeysImplemented() throws {
assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
Expand Down Expand Up @@ -215,6 +215,21 @@ final class CodingKeyMacroErrorTests: XCTestCase {
""",
diagnostics: [.init(error: .mustBeStringLiteral, line: 3, column: 5)],
macros: testMacros)

assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
@CustomCodingKey(1)
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .mustBeStringLiteral, line: 2, column: 5)],
macros: testMacros)
}

func testThrowsDueToEmptyStringCodingKey() throws {
Expand All @@ -234,7 +249,7 @@ final class CodingKeyMacroErrorTests: XCTestCase {
macros: testMacros)
}

func testThrowsDueToNoeCodableMacro() throws {
func testThrowsDueToNoCodableMacro() throws {
assertMacroExpansion(
"""
@SnakeCase struct TestCodable: Codable {
Expand All @@ -250,6 +265,21 @@ final class CodingKeyMacroErrorTests: XCTestCase {
diagnostics: [.init(error: .requiresCodableMacro(macroName: "SnakeCase"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@CodingKeySuffix struct TestCodable: Codable {
@CustomCodingKey("")
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .requiresCodableMacro(macroName: "CodingKeySuffix"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@CodingKeyPrefix struct TestCodable: Codable {
Expand All @@ -265,6 +295,76 @@ final class CodingKeyMacroErrorTests: XCTestCase {
diagnostics: [.init(error: .requiresCodableMacro(macroName: "CodingKeyPrefix"), line: 1, column: 1)],
macros: testMacros)
}

func testThrowsWhenNotStruct() throws {
assertMacroExpansion(
"""
@CustomCodable class TestCodable: Codable {
let originalKey: String
}
""",
expandedSource: """
class TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToStruct(name: "@CustomCodable"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@SnakeCase enum TestCodable: Codable {
case test
}
""",
expandedSource: """
enum TestCodable: Codable {
case test
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToPropertiesAndStructs(name: "@SnakeCase"), line: 1, column: 1)],
macros: testMacros)
}

func testThrowsWithEmptyCodingKey() throws {
assertMacroExpansion(
"""
@CustomCodable @SnakeCase struct TestCodable: Codable {
@CustomCodingKey()
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .codingKeyValueRequired, line: 2, column: 5)],
macros: testMacros)
}

func testThrowsWhenAttachedToFunction() throws {
assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
let originalKey: String
@CustomCodingKey("test")
func test() -> String { "" }
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
func test() -> String { "" }
private enum CodingKeys: String, CodingKey {
case originalKey = "originalKey"
}
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToProperty(name: "@CustomCodingKey"), line: 3, column: 5)],
macros: testMacros)
}
}

#endif
14 changes: 10 additions & 4 deletions Tests/CodableWrapperMacrosTests/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import XCTest
@testable import CodableWrapperMacros

extension DiagnosticSpec {
init(warning: SyntaxWarning, line: Int, column: Int) {
self.init(message: warning.localizedDescription, line: line, column: column, severity: .warning)
init(warning: SyntaxWarning, line: Int, column: Int,
originatorFile: StaticString = #filePath,
originatorLine: UInt = #line) {
self.init(message: warning.localizedDescription, line: line, column: column, severity: .warning,
originatorFile: originatorFile, originatorLine: originatorLine)
}

init(error: SyntaxError, line: Int, column: Int) {
self.init(message: error.localizedDescription, line: line, column: column, severity: .error)
init(error: SyntaxError, line: Int, column: Int,
originatorFile: StaticString = #filePath,
originatorLine: UInt = #line) {
self.init(message: error.localizedDescription, line: line, column: column, severity: .error,
originatorFile: originatorFile, originatorLine: originatorLine)
}
}

Expand Down
19 changes: 19 additions & 0 deletions Tests/CodableWrapperMacrosTests/KeyConverterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import Nimble
final class KeyConverterTests: QuickSpec {
override class func spec() {
describe("KeyConverter") {
describe("GeneralTests") {
context("CustomCase") {
it("ConvertsCorrectly") {
expect(CodingKeyCase.custom({ $0 + "Test" }).makeKeyValue(from: "Key")) == "KeyTest"
}
}
context("IsNumeric") {
it("ConvertsCorrectly") {
expect(KeyConverter.snakeCaseConverter.convert(value: "12345", variant: .camelCase)) == "12345"
}
}
}
describe("SnakeCase") {
let converter = KeyConverter.snakeCaseConverter
context("WithLowerCase") {
Expand Down Expand Up @@ -82,6 +94,13 @@ final class KeyConverterTests: QuickSpec {
}
}
}
describe("Invalid Cases") {
context("WithNoCaseSeparator") {
it("ReturnsNil") {
expect(KeyConverter(keyCase: .custom({ $0 }))) == nil
}
}
}
}
}
}
Expand Down
Loading

0 comments on commit 96a2d23

Please sign in to comment.