Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding missing tests and some minor refactoring #57

Merged
merged 1 commit into from
Jan 8, 2025
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
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 @@
/// 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 @@
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
Loading